Przeglądaj źródła

fixed wfs getBaseNamespaceUri

Piotr Labudda 9 lat temu
rodzic
commit
85f108694c

+ 29 - 8
SE/se-lib/Api/WfsDataServer.php

@@ -3,6 +3,7 @@
 Lib::loadClass('Api_WfsServerBase');
 Lib::loadClass('Api_WfsException');
 Lib::loadClass('Api_WfsGeomTypeConverter');
+Lib::loadClass('Api_WfsNs');
 
 class Api_WfsDataServer extends Api_WfsServerBase {
 
@@ -120,7 +121,7 @@ class Api_WfsDataServer extends Api_WfsServerBase {
 		$acl = $this->getAclFromTypeName($typeName);
 		$fldList = $this->_getFieldListFromAcl($acl);
 
-		$baseNsUri = $this->getBaseNamespaceUri();
+		$baseNsUri = Api_WfsNs::getBaseWfsUri();
 		$rootWfsNs = 'p5';
 		$rootWfsNsUri = "{$baseNsUri}";
 		//$wfsNs = 'p5_default_db_' . $type;//$nsPrefix;
@@ -194,12 +195,13 @@ class Api_WfsDataServer extends Api_WfsServerBase {
 
 	public function getFeatures($nsPrefix, $type, $maxFeatures, $srsname, $ogcFilter = '', $sortBy = '', $startIndex = 0, $propertyName = '', $simple = true) {
 		$DBG = (V::get('DBG_GEO', '', $_GET) > 0);// TODO: Profiler
+		$DBG_DS = V::get('DBG_DS', 0, $_GET, 'int');
 		$typeName = "{$nsPrefix}:{$type}";
 		if($DBG){echo "typeName($typeName})\n";}
 		$acl = $this->getAclFromTypeName($typeName);
 		$fldList = $this->_getFieldListFromAcl($acl);
 
-		$baseNsUri = $this->getBaseNamespaceUri();
+		$baseNsUri = Api_WfsNs::getBaseWfsUri();
 		$rootWfsNs = 'p5';
 		$rootWfsNsUri = "{$baseNsUri}";
 		//$wfsNs = 'p5_default_db_' . $type;//$nsPrefix;
@@ -273,8 +275,10 @@ if($DBG){echo '(geomFld: '.$geomFld.'):';print_r($acl->getFieldType($geomFld));e
 			$items[0] = $fakeItem;
 		}
 		foreach ($items as $itemKey => $item) {
+			if (!is_array($item)) $item = (array)$item;
 
-if($DBG && !empty($geomFld)){echo 'item['.$itemKey.'] ('.$geomFld.')isEmpty('.empty($item->{$geomFld}).'):';print_r($item->{$geomFld});echo "\n";}
+if($DBG && !empty($geomFld)){echo 'item['.$itemKey.'] ('.$geomFld.')isEmpty('.empty($item[$geomFld]).'):';print_r($item[$geomFld]);echo "\n";}
+			if($DBG_DS){echo ">>> loop({$itemKey}) item: ";print_r($item);echo "\n";}
 
 			$featureMemberNode = $dom->createElementNS('http://www.opengis.net/gml', 'gml:featureMember');
 			$rootNode->appendChild($featureMemberNode);
@@ -283,23 +287,40 @@ if($DBG && !empty($geomFld)){echo 'item['.$itemKey.'] ('.$geomFld.')isEmpty('.em
 				$featureNode->setAttribute('fid', "{$type}.{$itemKey}");
 					foreach ($fldList as $idZasob => $fldName) {
 						$featureFldNode = $dom->createElementNS($wfsNsUri, "{$wfsNs}:{$fldName}");
-						if ($acl->isAllowed($idZasob, 'R', $item)) {
+						if($DBG_DS){echo">>> acl->validateFieldAction('{$fldName}', 'R', \$item) ...\n";}
+						if ($acl->validateFieldAction($fldName, 'R', (object)$item)) {
 							if ($geomFld != null && $geomFld == $fldName) {
-								$geomNode = $this->_typeConverter->createGmlFromWkt($item->{$fldName}, $dom);
+								$geomNode = $this->_typeConverter->createGmlFromWkt($item[$fldName], $dom);
 								if (!$geomNode) continue;
 								$featureFldNode->appendChild($geomNode);
+							} else if (is_array($item[$fldName])) {// TODO: by struct - REF field
+								if($DBG_DS){echo">>> TODO({$fldName}) REF item[{$itemKey}][{$fldName}]: ";print_r($item[$fldName]);echo "\n";}
+								if (1 == count($item[$fldName])) {
+									$xlink = $item[$fldName][0]['xlink'];
+									$xlinkParts = explode(':', $xlink);
+									if (2 != count($xlinkParts)) throw new Exception("Error Processing Reques - wrong xlink format for ".$acl->getName().".{$itemKey}/{$fldName}");
+									$xlinkParts[0] = Api_WfsNs::getNsUri($xlinkParts[0]);
+									$xlink = implode('#', $xlinkParts);
+									$featureFldNode->setAttribute('xlink:href', $xlink);
+								} else {
+									throw new Exception("Error Processing Request - too many refs for ".$acl->getName().".{$itemKey}/{$fldName}");
+								}
+								// foreach ($item[$fldName] as $ref) {
+								// 	$refNode = $dom->createElementNS($wfsNsUri, "{$wfsNs}:{$fldName}");
+								// 	$featureFldNode->appendChild($refNode);
+								// }
 							} else {
-								$featureFldNode->nodeValue = str_replace('&', '&', $item->{$fldName});
+								$featureFldNode->nodeValue = str_replace('&', '&', $item[$fldName]);
 								if (empty($featureFldNode->nodeValue) && '0' !== $featureFldNode->nodeValue) {
 									continue;
 								}
 							}
 						}
 						if (!$simple) {
-							if (!$acl->isAllowed($idZasob, 'R', $item)) {
+							if (!$acl->validateFieldAction($fldName, 'R', (object)$item)) {
 								$featureFldNode->setAttributeNS($rootWfsNsUri, "{$rootWfsNs}:allow_read", "false");
 							}
-							if ($acl->isAllowed($idZasob, 'W', $item)) {
+							if ($acl->validateFieldAction($fldName, 'W', (object)$item)) {
 								$featureFldNode->setAttributeNS($rootWfsNsUri, "{$rootWfsNs}:allow_write", "true");
 							}
 						}

+ 37 - 0
SE/se-lib/Api/WfsNs.php

@@ -0,0 +1,37 @@
+<?php
+
+Lib::loadClass('Request');
+
+class Api_WfsNs {
+
+  // @usage: Api_WfsNs::getNsList();
+  public static function getNsList() {
+    $listNs = array();
+    $baseNsUri = self::getBaseWfsUri();
+    $listNs["{$baseNsUri}"] = 'p5';
+		$listNs["{$baseNsUri}/default_db"] = 'p5_default_db';
+		$listNs["{$baseNsUri}/objects"] = 'p5_objects';
+		return $listNs;
+  }
+
+  // @usage: Api_WfsNs::getNsList('default_db');
+  public static function getNsUri($nsPrefix) {
+    $listNs = self::getNsList();
+    $nsMap = array_flip($listNs);
+    return V::get($nsPrefix, null, $nsMap);
+  }
+
+  // @usage: Api_WfsNs::getNsPrefix('https://biuro.biall-net.pl/wfs/default_db');
+  public static function getNsPrefix($nsUri) {
+    $listNs = self::getNsList();
+    $nsUri = trim($nsUri);
+    $nsUri = rtrim($nsUri, '/');
+    return V::get($nsUri, null, $listNs);
+  }
+
+  // @usage: Api_WfsNs::getBaseWfsUri();
+  public static function getBaseWfsUri() {
+    return Request::getHostUri() . "/wfs";
+  }
+
+}

+ 0 - 1
SE/se-lib/Api/WfsQgis.php

@@ -74,7 +74,6 @@ class Api_WfsQgis extends ApiRouteBase {// TODO: extends Api_WfsBase which exten
 		$wfsServer = new Api_WfsQgisServer($userAcl);
 		$wfsServer->setLogger($this->_logger);
 		$wfsServer->setBaseUri($this->_apiBaseUri);
-		DBG::_('DBG', true, 'getBaseNamespaceUri:', $wfsServer->getBaseNamespaceUri(), __CLASS__, __FUNCTION__, __LINE__);
 		if ('WFS' != V::get('SERVICE', '', $request->query) and ('WFS' != V::get('service', '', $request->query)))  {
 			throw new Api_WfsException("Only WFS Service is allowed");
 		}

+ 1 - 1
SE/se-lib/Api/WfsQgisServer.php

@@ -56,7 +56,7 @@ class Api_WfsQgisServer extends Api_WfsServerBase {
 		$acl = $this->getAclFromTypeName($typeName);
 		$fldList = $this->_getFieldListFromAcl($acl);
 
-		$baseNsUri = $this->getBaseNamespaceUri();
+		$baseNsUri = Api_WfsNs::getBaseWfsUri();
 		//$wfsNs = 'p5_default_db_' . $type;//$nsPrefix;
 		$wfsNs = 'p5_default_db';//$nsPrefix;
 		$wfsNsUri = "{$baseNsUri}/" . substr($nsPrefix, 3);

+ 230 - 91
SE/se-lib/Api/WfsServerBase.php

@@ -2,6 +2,7 @@
 
 Lib::loadClass('Api_WfsException');
 Lib::loadClass('Api_WfsGeomTypeConverter');
+Lib::loadClass('Api_WfsNs');
 
 class Api_WfsServerBase {
 
@@ -20,30 +21,10 @@ class Api_WfsServerBase {
 		$this->_apiBaseUri = $uri;
 	}
 
-	public function getBaseUri() {
+	public function getBaseUri() {// TODO: RMME
 		return $this->_apiBaseUri;
 	}
 
-	public function getBaseNamespaceUri() {
-		$baseNsUri = $this->_apiBaseUri;
-		$protocolType = '';
-		if (!empty($baseNsUri)) {
-			if ('https://' == substr($baseNsUri, 0, 8)) {
-				$protocolType = 'https';
-				$baseNsUri = substr($baseNsUri, 8);
-			} else if ('http://' == substr($baseNsUri, 0, 7)) {
-				$protocolType = 'http';
-				$baseNsUri = substr($baseNsUri, 6);
-			}
-		}
-		if (!empty($baseNsUri)) {
-			if (false !== ($pos = strpos($baseNsUri, '/'))) {
-				$baseNsUri = substr($baseNsUri, 0, $pos);
-			}
-		}
-		return "{$protocolType}://{$baseNsUri}/wfs";
-	}
-
 	public function isAllowedFeatureType($nsPrefix, $type) {
 		if ('p5_' != substr($nsPrefix, 0, 3)) return false;
 		if ('p5_default_db' == $nsPrefix) {
@@ -58,6 +39,8 @@ class Api_WfsServerBase {
 			}
 		} else if ('p5_objects' == $nsPrefix && 'File' == $type) {
 			return true;
+		} else if ('p5_objects' == $nsPrefix && 'TestPerms' == $type) {
+			return true;
 		}
 		return false;
 	}
@@ -85,7 +68,7 @@ class Api_WfsServerBase {
 				  xmlns="http://www.opengis.net/wfs"
 				  xmlns:ogc="http://www.opengis.net/ogc"
 				  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-				  <?php echo $this->_getXmlNamespaceList(); ?>
+				  <?php echo $this->_printXmlNamespaceList(); ?>
 				  <?php echo $this->_getXmlSchemaLocation(); ?>
 					version="1.0.0">
   <Service>
@@ -373,24 +356,12 @@ class Api_WfsServerBase {
 		return (!empty($schemaLocations))? 'xsi:schemaLocation="' . implode(' ', $schemaLocations) . '"' : '';
 	}
 
-	public function _getXmlNamespaceList() {
-		$baseNsUri = $this->getBaseNamespaceUri();
+	public function _printXmlNamespaceList() {
 		$listNs = array();
-		$listNs[] = 'xmlns:p5_default_db="' . $baseNsUri . '/default_db"';
-		$listNs[] = 'xmlns:p5_objects="' . $baseNsUri . '/objects"';
-		return implode("\n", $listNs);
-	}
-
-	public function _getXmlNamespaceList__OLD() {
-		$baseNsUri = $this->getBaseNamespaceUri();
-		$namespaceList = $this->_getSourceNsList();
-		$namespaceListOut = array();
-		foreach ($namespaceList as $nsObj) {
-			$ns = "p5_{$nsObj[0]}_{$nsObj[1]}";
-			$uri = "{$baseNsUri}/{$nsObj[0]}/{$nsObj[1]}";
-			$namespaceListOut[] = 'xmlns:' . $ns . '="' . $uri . '"';
+		foreach (Api_WfsNs::getNsList() as $uri => $prefix) {
+			$listNs[] = 'xmlns:' . $prefix . '="' . $uri . '/default_db"';
 		}
-		return implode(' ', $namespaceListOut);
+		return implode("\n", $listNs);
 	}
 
 	public function _getSourceNsList() {
@@ -402,6 +373,7 @@ class Api_WfsServerBase {
 			$usrObjList[] = array($dataSourceName, $tblName);
 		}
 		$usrObjList[] = array('objects', 'File');
+		if ('production' != V::get('P5_ENV', 'production', $_SERVER)) $usrObjList[] = array('objects', 'TestPerms');
 		return $usrObjList;
 	}
 
@@ -601,27 +573,218 @@ class Api_WfsServerBase {
 	}
 
 	public function _parseTransactionXmlStruct($requestXml, $requestXmlTags) {
-		$DBG = (V::get('DBG_XML', '', $_GET) > 0);// TODO: Profiler
+		$DBG = V::get('DBG_XML', 0, $_GET, 'int');// TODO: Profiler
 		$rootTagName = V::get('tag', '', $requestXmlTags[0]);
 		if ('Transaction' != $rootTagName) {
 			throw new Api_WfsException("Parse Request XML Error - Missing Transaction as root xml tag", __LINE__, null, 'TransactionParseError', 'request');
 		}
 
+		// TODO: special actions if action on nested objects
 		// 1. convert request: wfs.transaction.convert-wfs-request.xsl
 		// 2. validate converted request: wfs.transaction-converted-request.xsd
 		// 3. execute request in data source
-		// ? acl check?
+
+		// if($DBG){echo 'L.' . __LINE__ . ' $requestXmlTags:';print_r($requestXmlTags);echo "\n";}
+		/*
+				<Transaction
+				  xmlns="http://www.opengis.net/wfs"
+				  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+				  version="1.0.0"
+				  service="WFS"
+				  xmlns:p5_objects="https://biuro.biall-net.pl/wfs/objects"
+				  xmlns:gml="http://www.opengis.net/gml">
+				  <Insert xmlns="http://www.opengis.net/wfs">
+				    <TestPerms xmlns="https://biuro.biall-net.pl/wfs/objects">
+				      <plik xmlns="https://biuro.biall-net.pl/wfs/objects">
+				        <p5_objects:File>
+				          <p5_objects:name>blank-wfs.gif</p5_objects:name>
+				          <p5_objects:content>R0lGODlhAQABAIAAAP///////yH+EUNyZWF0ZWQgd2l0aCBHSU1QACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==</p5_objects:content>
+				        </p5_objects:File>
+				      </plik>
+				    </TestPerms>
+				  </Insert>
+				</Transaction>
+		*/
+		$actionXmlTags = array();// // [ 0 => [ 'action'=>Insert, 'typeName'=>str, 'tags'=>[] ]
+		{// split xml for action tags (Insert, Update, Delete)
+			$tagsCount = count($requestXmlTags);
+			for ($i = 1, $actionTagName = null, $actionIdx = -1, $tagLvl = 0; $i < $tagsCount - 1; $i++) {// skip Transaction open/close tag
+				$tag = $requestXmlTags[$i];
+				if (null == $actionTagName) {
+					$actionTagName = $tag['tag'];
+					$tagLvl = $tag['level'];
+					$actionIdx += 1;
+					$actionXmlTags[$actionIdx] = array();
+					$actionXmlTags[$actionIdx]['action'] = $actionTagName;
+					$actionXmlTags[$actionIdx]['typeName'] = V::get('typeName', '', $tag['attributes']);
+					$actionXmlTags[$actionIdx]['isDeepObject'] = null;// null - unknown, false - not seed, true - deep
+					$actionXmlTags[$actionIdx]['tags'] = array();
+				} else if ($tag['tag'] == $actionTagName && 'close' == $tag['type'] && $tagLvl == $tag['level']) {
+					$actionTagName = null;
+				} else {
+					$actionXmlTags[$actionIdx]['tags'][] = $tag;
+				}
+			}
+		}
+
+		{// Validate Request: WFS allow multiple tags inside Insert tag
+			// TODO: implement multiple tags in Insert tag if reauired. Use array_splice($actionXmlTags, $actionIdx, 0, $insertTags);
+			{// throw (Not Implemented, 501) if found multiple tags in Insert tag
+				foreach ($actionXmlTags as $actionIdx => $action) {
+					if ('Insert' !== $action['action']) continue;
+					$lvl = $action['tags'][0]['level'];
+					for ($i = 1, $cnt = count($action['tags']); $i < $cnt - 1; $i++) {
+						$tag = $action['tags'][$i];
+						// if($DBG){echo 'L.' . __LINE__ . " actionXmlTags loop({$i}) \$tag:";print_r($tag);echo "\n";}
+						if ($tag['level'] == $lvl) throw new Exception("Error Processing Request - multiple tags inside Insert tag is not implemented", 501);
+					}
+				}
+			}
+		}
+
+		{// Insert tag - fix typeName from first tag, remove first and last tag - leave only fields
+			foreach ($actionXmlTags as $actionIdx => $action) {
+				if ('Insert' !== $action['action']) continue;
+				array_pop($action['tags']);// remove last tag (close tag)
+				$tag = array_shift($action['tags']);// remove last tag (close tag)
+				$typeName = $tag['tag'];// eg. with prefix 'p5_objects:File' or without prefix but with @xmlns
+				if (false === strpos($typeName, ':')) {
+					$nsType = V::get('xmlns', '', $tag['attributes']);
+					if (!$nsType) throw new Exception("Error Processing Request - Missing object namespace '{$tag['tag']}'");
+					$prefix = Api_WfsNs::getNsPrefix($nsType);
+					if (!$prefix) {
+						if ($typeName == substr(rtrim($nsType, '/'), -1 * strlen($typeName))) {// typeName may be added to ns uri
+							$nsBaseForType = substr(rtrim($nsType, '/'), 0, -1 * strlen($typeName) - 1);
+							$prefix = Api_WfsNs::getNsPrefix($nsBaseForType);
+						}
+					}
+					if (!$prefix) throw new Exception("Error Processing Request - Unrecognized namespace uri '{$nsType}' for object '{$tag['tag']}'");
+					$typeName = "{$prefix}:{$typeName}";
+				}
+				$action['typeName'] = $typeName;
+				$actionXmlTags[$actionIdx] = $action;
+				// $lvl = $action['tags'][0]['level'];
+				// for ($i = 1, $cnt = count($action['tags']); $i < $cnt - 1; $i++) {
+				// 	$tag = $action['tags'][$i];
+				// 	// if($DBG){echo 'L.' . __LINE__ . " actionXmlTags loop({$i}) \$tag:";print_r($tag);echo "\n";}
+				// 	// if ($tag['level'] > $lvl) $isDeepStructure = true;
+				// }
+			}
+		}
+
+		{
+			// if($DBG){echo 'L.' . __LINE__ . ' before validate $actionXmlTags:';print_r($actionXmlTags);echo "\n";}
+			foreach ($actionXmlTags as $actionIdx => $action) {
+				if ('Insert' == $action['action']) {
+					if (empty($action['typeName'])) throw new Exception("Error Processing Request - unknown object typeName to Insert");
+					$acl = $this->getAclFromTypeName($action['typeName']);
+					$actionXmlTags[$actionIdx] = $acl->validateInsertXml($action);
+				} else if ('Update' == $action['action']) {
+					$acl = $this->getAclFromTypeName($action['typeName']);
+					$actionXmlTags[$actionIdx] = $acl->validateUpdateXml($action);
+				} else if ('Delete' == $action['action']) {
+					if($DBG>1){echo'<pre>$action: ';print_r($action);echo'</pre>';}
+					$acl = $this->getAclFromTypeName($action['typeName']);
+					$actionXmlTags[$actionIdx] = $acl->validateDeleteXml($action);
+				} else {
+					if($DBG>1){echo'<pre>$action: ';print_r($action);echo'</pre>';}
+					throw new Exception("{$action['action']} action not implemented", 501);
+				}
+				// continue;// TODO: validate all by type
+			}
+			$returnIds = array();
+			$changesList = array();
+			foreach ($actionXmlTags as $actionIdx => $action) {
+				if ('Insert' == $action['action']) {
+					if (empty($action['typeName'])) throw new Exception("Error Processing Request - unknown object typeName to Insert");
+					$acl = $this->getAclFromTypeName($action['typeName']);
+					$newId = $acl->insertXml($action);
+					$returnIds[$actionIdx] = $newId;
+					$changesList[$actionIdx] = array('Status'=>(($newId > 0)? 'SUCCESS' : 'FAILED'), 'Message'=>"created {$newId}.", 'Action' => $action['action']);
+					if ($newId > 0) $changesList[$actionIdx]['fid'] = $acl->getName() . '.' . $newId;
+				} else if ('Update' == $action['action']) {
+					if($DBG>1){echo'<pre>$action: ';print_r($action);echo'</pre>';}
+					$acl = $this->getAclFromTypeName($action['typeName']);
+					$affected = $acl->updateXml($action);
+					$changesList[$actionIdx] = array('Status'=>(($affected >= 0)? 'SUCCESS' : 'FAILED'), 'Message'=>"affected {$affected}.", 'Action' => $action['action']);
+				} else if ('Delete' == $action['action']) {
+					$acl = $this->getAclFromTypeName($action['typeName']);
+					$affected = $acl->deleteXml($action);
+					$changesList[$actionIdx] = array('Status'=>(($affected >= 0)? 'SUCCESS' : 'FAILED'), 'Message'=>"deleted {$affected}.", 'Action' => $action['action']);
+				} else throw new Exception("TODO: {$action['action']} action not implemented", 501);
+			}
+			if($DBG){echo 'L.' . __LINE__ . ' $changesList:';print_r($changesList);echo "\n";}
+
+			return $this->_transactionResponse($changesList);
+
+			if($DBG){echo 'L.' . __LINE__ . ' $actionXmlTags:';print_r($actionXmlTags);echo "\n";}
+			$requestNsUriList = array();// [ uri, ... ]
+			$requestNsUriToPrefixMap = array();// uri => prefix
+			$requestNsPrefixToUriMap = array();// prefix => uri
+			{
+				$tag = reset($requestXmlTags);// first tag: 'Transaction'
+				$itemsNsList = Api_WfsNs::getNsList();
+				if($DBG){echo 'L.' . __LINE__ . ' $itemsNsList:';print_r($itemsNsList);echo "\n";}
+				foreach ($tag['attributes'] as $attrName => $attrVal) {
+					if ('xmlns:' !== substr($attrName, 0, 6)) continue;
+					if ('xmlns:xsi' === $attrName) continue;
+					if ('xmlns:gml' === $attrName) continue;
+					if (array_key_exists($attrVal, $itemsNsList)) {
+						$requestNsUriList[$attrVal] = true;// ns uri used
+						$prefixNS = substr($attrName, 6);
+						$requestNsUriToPrefixMap[$attrVal] = $prefixNS;
+						$requestNsPrefixToUriMap[$prefixNS] = $attrVal;
+					}
+					echo "NS List: loop({$attrName}) isItemNsList(".(array_key_exists($attrVal, $itemsNsList)).") \t val:";var_dump($attrVal);echo"\n";
+				}
+			}
+			if($DBG){echo 'L.' . __LINE__ . ' $requestNsUriList:'.json_encode($requestNsUriList);echo "\n";}
+			if($DBG){echo 'L.' . __LINE__ . ' $requestNsUriToPrefixMap:'.json_encode($requestNsUriToPrefixMap);echo "\n";}
+			if($DBG){echo 'L.' . __LINE__ . ' $requestNsPrefixToUriMap:'.json_encode($requestNsPrefixToUriMap);echo "\n";}
+			// fix Insert tag - get typeName from first tag under Insert
+			foreach ($actionXmlTags as $actionIdx => $action) {
+				if ('Insert' !== $action['action']) continue;
+				$tag = reset($action['tags']);
+				$name = $tag['tag'];
+				// <p5_objects:File... or <File xmlns=".../wfs/objects"...
+				// V::get('xmlns', '', $tag['attributes']);
+				$actionXmlTags[$actionIdx]['ns_uri'] = V::get('xmlns', '', $tag['attributes']);
+			}
+			// fix ns_uri by @typeName prefix or @xmlns
+		}
 
 		$usedSourceNsList = array();
 		$sourceNsList = $this->_getSourceNsList();
-		//$sourceNsList = array();
-		//foreach ($requestXmlTags[0]['attributes'] as $attrName => $attrValue) {
-		//	if ('xmlns:p5_' == substr($attrName, 0, 9)) {
-		//		$sourceNsList[substr($attrName, 6)] = $attrValue;
-		//	}
-		//}
+		if($DBG>3){echo 'L.' . __LINE__ . ' $requestXmlTags:';print_r($requestXmlTags);echo "\n";}
+		if($DBG>3){echo 'L.' . __LINE__ . ' $sourceNsList:';print_r($sourceNsList);echo "\n";}
 		{// get used typeNames
 			$usedTypeNames = array();
+			foreach ($actionXmlTags as $action) {
+				if ('Delete' == $action['action']) {
+					foreach ($sourceNsList as $nsInd => $ns) {
+						if ("p5_{$ns[0]}:{$ns[1]}" == $action['typeName']) {
+							$usedSourceNsList[$nsInd] = $ns;
+						}
+					}
+				} else if ('Update' == $action['action']) {
+					foreach ($sourceNsList as $nsInd => $ns) {
+						if ("p5_{$ns[0]}:{$ns[1]}" == $action['typeName']) {
+							$usedSourceNsList[$nsInd] = $ns;
+						}
+					}
+				} else if ('Insert' == $action['action']) {
+					foreach ($action['tags'] as $tag) {
+						if ('open' != $tag['type']) continue;
+						foreach ($sourceNsList as $nsInd => $ns) {
+							if ("{$ns[1]}" == $tag['tag']) {
+								$usedSourceNsList[$nsInd] = $ns;
+							}
+						}
+					}
+				}
+			}
+			if($DBG){echo 'L.' . __LINE__ . ' $usedSourceNsList after analyze split action tags:';print_r($usedSourceNsList);echo "\n";}
+			if($DBG){echo 'L.' . __LINE__ . ' exit';exit;}
 
 			// <Update xmlns="http://www.opengis.net/wfs" typeName="p5_default_db:TEST_PERMS">
 			foreach ($requestXmlTags as $tag) {
@@ -634,6 +797,7 @@ class Api_WfsServerBase {
 					}
 				}
 			}
+			if($DBG){echo 'L.' . __LINE__ . ' $usedSourceNsList after Update:';print_r($usedSourceNsList);echo "\n";}
 			// TODO: check: <Transaction xmlns:p5_default_db="https://biuro.biall-net.pl/wfs/default_db/TEST_PERMS"
 			// <Insert xmlns="http://www.opengis.net/wfs">
 			//   <TEST_PERMS xmlns="https://biuro.biall-net.pl/wfs/default_db/TEST_PERMS">
@@ -641,6 +805,7 @@ class Api_WfsServerBase {
 			foreach ($requestXmlTags as $tag) {
 				if ('Insert' == $lastTagName) {
 					$typeName = $tag['tag'];
+					if($DBG && 'File' == $typeName){echo 'L.' . __LINE__ . ' loop Insert: $typeName(File):';print_r($tag);echo "\n";}
 					foreach ($sourceNsList as $nsInd => $ns) {
 						if ("{$ns[1]}" == $typeName) {
 							$usedSourceNsList[$nsInd] = $ns;
@@ -649,6 +814,7 @@ class Api_WfsServerBase {
 				}
 				$lastTagName = $tag['tag'];
 			}
+			if($DBG){echo 'L.' . __LINE__ . ' $usedSourceNsList after Insert:';print_r($usedSourceNsList);echo "\n";}
 
 			// <Delete xmlns="http://www.opengis.net/wfs" typeName="p5_default_db:TEST_GEOM_POINT">
 			foreach ($requestXmlTags as $tag) {
@@ -661,6 +827,7 @@ class Api_WfsServerBase {
 					}
 				}
 			}
+			if($DBG){echo 'L.' . __LINE__ . ' $usedSourceNsList after Delete:';print_r($usedSourceNsList);echo "\n";}
 		}
 		if (empty($usedSourceNsList)) {
 			throw new Api_WfsException("Parse Request XML Error - Empty source NS list", __LINE__, null, 'TransactionParseError', 'request');
@@ -764,7 +931,7 @@ if($DBG){echo 'L.' . __LINE__ . ' $convertedTransaction:';print_r($convertedTran
 						if ('Insert' == substr($tag['tag'], 0, 6)) {
 							$typeNsPrefix = 'p5_default_db';
 							if (!empty($tag['attributes']['typeNsUri'])) {
-								$baseNsUri = $this->getBaseNamespaceUri();
+								$baseNsUri = Api_WfsNs::getBaseWfsUri();
 								if ("{$baseNsUri}/objects" == $tag['attributes']['typeNsUri']) {
 									$typeNsPrefix = 'p5_objects';
 								}
@@ -888,7 +1055,7 @@ if($DBG){echo 'L.' . __LINE__ . ' $convertedTransaction:';print_r($convertedTran
 			if ('SUCCESS' == $change['Status'] && !empty($change['fid'])) {
 				$createdFetureId[] = $change['fid'];
 			}
-			if (!empty($change['Message'])) $messageTag .= "Feature '{$featureId}' {$change['Status']}: {$change['Message']}\n";
+			// if (!empty($change['Message'])) $messageTag .= "Feature '{$featureId}' {$change['Status']}: {$change['Message']}\n";
 		}
 		$statusTag = ($statusIsFailed)? 'FAILED' : 'SUCCESS';
 		$statusTag = "<wfs:{$statusTag}/>";
@@ -1099,10 +1266,7 @@ if($DBG){echo 'L.' . __LINE__ . ' sourceNsList:';print_r($sourceNsList);echo "\n
 		$dom->preserveWhiteSpace = false;
 		$rootNode = $dom->createElementNS('http://www.w3.org/2001/XMLSchema', 'xsd:schema');
 		$dom->appendChild($rootNode);
-		// $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:gml', 'http://www.opengis.net/gml');
-		// $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:' . $wfsNs, $wfsNsUri);
 		$rootNode->setAttribute('elementFormDefault', 'qualified');
-		// $rootNode->setAttribute('targetNamespace', $wfsNsUri);
 		$rootNode->setAttribute('version', '1.0');
 
 		{// <xsd:element name="Transaction" type="TransactionType">
@@ -1429,13 +1593,11 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 	public function _getDescribeFeatureTypes($typeNames, $simple = true) {
 		if (empty($typeNames)) throw new HttpException("Feature Type Names not defined", 400);
 		$this->DBG("types:" . json_encode($typeNames), __LINE__, __FUNCTION__, __CLASS__);
-		$baseNsUri = $this->getBaseNamespaceUri();
 
+		$baseNsUri = Api_WfsNs::getBaseWfsUri();
 		$rootWfsNs = 'p5';
 		$rootWfsNsUri = "{$baseNsUri}";
-		$wfsNs = 'p5_default_db';
-		$wfsNsUri = "{$baseNsUri}/default_db";
-		$featureTypeUri = $this->getBaseUri() . "?SERVICE=WFS&VERSION=1.0.0&REQUEST=DescribeFeatureType";
+		$featureTypeUri = Api_WfsNs::getBaseWfsUri() . "?SERVICE=WFS&VERSION=1.0.0&REQUEST=DescribeFeatureType";
 
 		header('Content-type: application/xml');
 		// <xsd:import namespace="http://www.opengis.net/gml" schemaLocation="http://webgis.regione.sardegna.it:80/geoserver/schemas/gml/2.1.2/feature.xsd"/>
@@ -1447,10 +1609,11 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 		$rootNode = $dom->createElementNS('http://www.w3.org/2001/XMLSchema', 'xsd:schema');
 		$dom->appendChild($rootNode);
 		$rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:gml', 'http://www.opengis.net/gml');
-		$rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:' . $wfsNs, $wfsNsUri);
-		$rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', "xmlns:{$rootWfsNs}", $rootWfsNsUri);
+		foreach (Api_WfsNs::getNsList() as $uri => $prefix) {
+			$rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', "xmlns:{$prefix}", $uri);
+		}
 		$rootNode->setAttribute('elementFormDefault', 'qualified');
-		$rootNode->setAttribute('targetNamespace', $wfsNsUri);
+		$rootNode->setAttribute('targetNamespace', Api_WfsNs::getNsUri('p5_default_db'));// TODO:? what targetNamespace if showing types from p5_objects and p5_default_db
 		{// <xsd:import namespace="http://www.opengis.net/gml" schemaLocation="...../gml/2.1.2/feature.xsd"/>
 			$elNode = $dom->createElementNS('http://www.w3.org/2001/XMLSchema', 'xsd:import');
 			$rootNode->appendChild($elNode);
@@ -1497,7 +1660,6 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 			foreach ($fldList as $idZasob => $fldName) {
 				$elNode = $dom->createElementNS('http://www.w3.org/2001/XMLSchema', 'xsd:element');
 				$seqNode->appendChild($elNode);
-				$elNode->setAttribute('name', $fldName);
 				$minOccurs = 0;
 				if ($pKeyField == $fldName) {
 					$minOccurs = '1';
@@ -1507,39 +1669,16 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 				$elNode->setAttribute('minOccurs', $minOccurs);
 				$elNode->setAttribute('maxOccurs', '1');
 				$elNode->setAttribute('nillable', 'true');
-				$fldType = 'xsd:string';
-				if ($acl->isIntegerField($fldName)) {
-					$fldType = 'xsd:integer';
+				$fldType = $acl->getXsdFieldType($fldName);
+				if (!$simple && $acl->isEnumerationField($fldName)) {
+					$fldType = "{$nsPrefix}:{$fldName}Type";
 				}
-				else if ($acl->isDecimalField($fldName)) {
-					$fldType = 'xsd:decimal';
-				}
-				else if ($acl->isDateField($fldName)) {
-					$fldType = 'xsd:date';
-				}
-				else if ($acl->isDateTimeField($fldName)) {
-					$fldType = 'xsd:dateTime';
-				}
-				else if ($acl->isGeomField($fldName)) {
-					//$fldType = 'gml:GeometryPropertyType';
-					$geomType = $acl->getGeomFieldType($fldName);
-					if ('polygon' == $geomType) {
-						$fldType = 'gml:PolygonPropertyType';
-					} else if ('point' == $geomType) {
-						$fldType = 'gml:PointPropertyType';
-					} else if ('linestring' == $geomType) {
-						$fldType = 'gml:LineStringPropertyType';
-					} else {
-						$fldType = 'gml:GeometryPropertyType';
-					}
-				}
-				else if ($acl->isEnumerationField($fldName)) {
-					$fldType = ($simple)? 'xsd:string' : "{$nsPrefix}:{$fldName}Type";
-				}
-				else if ($acl->isBinaryField($fldName)) {
-					$fldType = 'xsd:base64Binary';
+				if ('ref:' == substr($fldType, 0, 4)) {
+					$elNode->setAttribute('ref', substr($fldType, 4));
+				} else {
+					$elNode->setAttribute('name', $fldName);
+					$elNode->setAttribute('type', $fldType);
 				}
-				$elNode->setAttribute('type', $fldType);
 
 				if (!$simple) {
 					if (!empty($p5Attributes[$fldName])) {
@@ -1557,7 +1696,7 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 			$elNode = $dom->createElementNS('http://www.w3.org/2001/XMLSchema', 'xsd:element');
 			$rootNode->appendChild($elNode);
 			$elNode->setAttribute('name', $type);
-			$elNode->setAttribute('type', $wfsNs . ':' . $typeName);
+			$elNode->setAttribute('type', $acl->getSourceName() . ':' . $typeName);
 			if ($simple) {
 				$elNode->setAttribute('substitutionGroup', 'gml:_Feature');
 			} else {

+ 496 - 3
SE/se-lib/Core/AclBase.php

@@ -1,15 +1,21 @@
 <?php
 
+Lib::loadClass('Api_WfsNs');
+
 class Core_AclBase {
 
   public function getSourceName() { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
   public function getName() { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+  public function getRootTableName() { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+  public function getXsdFieldType($fldName) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+  // TODO: get more xsd restrictions like minOccurs, maxOccurs, nillable and restrictions
+
   public function getID() { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }// TODO: Legacy
   public function init($force = false) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
   public function isInitialized() { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
-  public function getRealFieldListByIdZasob($force = false) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+  public function getRealFieldListByIdZasob() { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
   public function getFieldIdByName($fieldName) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
-  public function isIntegerField($fieldName) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+  public function isIntegerField($fieldName) { return ('xsd:integer' == $this->getXsdFieldType($fldName)); }
   public function isDecimalField($fieldName) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
   public function isGeomField($fldName) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
   public function isDateField($fldName) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
@@ -23,8 +29,495 @@ class Core_AclBase {
   public function hasFieldPerm($idZasob, $taskPerm) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
   public function getItems($params = array()) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
   public function addItem($todoItem) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
-  public function getGeomFieldType() { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+  public function updateItem($itemPatch) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+  public function getGeomFieldType($fldName) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
   public function getPrimaryKeyField() { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
   public function getAttributesFromZasoby() { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+  public function validateFieldAction($fieldName, $perms, $record = null) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+
+  public function validateDeleteXml($action) {// @returns action tags, throws Exceptions
+    $DBG = V::get('DBG_XML', 0, $_GET, 'int');
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Delete action \$action:";print_r($action);echo "\n";}
+    if (empty($action['tags'])) throw new Exception("Error Processing Delete Request - wrong Delete tag format");
+    if ('open' != $action['tags'][0]['type']) throw new Exception("Error Processing Delete Request - wrong Delete tag format");
+    if ('Filter' != $action['tags'][0]['tag']) throw new Exception("Error Processing Delete Request - wrong Delete tag format");
+    $filterLvl = $action['tags'][0]['level'];
+    array_shift($action['tags']);// remove first openig tag Filter
+    array_pop($action['tags']);// remove last closing tag Filter
+    if (empty($action['tags'])) throw new Exception("Error Processing Delete Request - missing FeatureId in Delete tag");
+    $action['Filter'] = array();
+    $featureName = $this->getName();
+    foreach ($action['tags'] as $idx => $tag) {
+      if ($tag['level'] <= $filterLvl) throw new Exception("Error Processing Delete Request - wrong Delete tag format Delete/Filter/{$tag['tag']}[{$idx}]");
+      if ('FeatureId' != $tag['tag']) throw new Exception("Error Processing Delete Request - wrong Delete tag format - expected 'FeatureId' but '{$tag['tag']}' found");
+      if (empty($tag['attributes'])) throw new Exception("Error Processing Delete Request - missing attributes in Delete/Filter/FeatureId[{$idx}]");
+      if (empty($tag['attributes']['fid'])) throw new Exception("Error Processing Delete Request - missing @fid attribute in Delete/Filter/FeatureId[{$idx}]");
+      $idFeature = $tag['attributes']['fid'];
+      if ("{$featureName}." != substr($idFeature, 0, strlen("{$featureName}."))) throw new Exception("Error Processing Delete Request - wrong typeName in Delete/Filter/FeatureId[{$idx}]/@fid");
+      $idFeature = substr($idFeature, strlen("{$featureName}."));
+      if (!$this->checkPrimaryKeyFormat($idFeature)) throw new Exception("Error Processing Delete Request - wrong primary key format in Delete/Filter/FeatureId[{$idx}]/@fid");
+      $action['Filter'][] = $idFeature;
+    }
+    $action['fields'] = array();
+    $action['fields']['the_geom'] = array();
+    $action['fields']['the_geom'][] = array('type'=>'complete', 'value'=>'NULL');
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Delete action validate return \$action:";print_r($action);echo "\n";}
+
+    return $action;
+  }
+
+  public function validateUpdateXml($action) {// @returns action tags, throws Exceptions
+    $DBG = V::get('DBG_XML', 0, $_GET, 'int');
+    // split Update tag to Property and Filter
+    $fieldsLvl = $action['tags'][0]['level'];
+    $totalTags = count($action['tags']);
+    $tagsToSplice = array();// args for splice(offset, length to remove, new values)
+    for ($i = 0, $cnt = $totalTags, $lastIdxToSplice = 0; $i < $cnt; $i++) {
+      $tag = $action['tags'][$i];
+      if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Insert to flat fields loop({$i}) \$lastIdxToSplice({$lastIdxToSplice}) \$tag('{$tag['tag']}', '{$tag['type']}', '{$tag['level']}')"."\n";}
+      if ($fieldsLvl == $tag['level'] && 'complete' == $tag['type']) {// field - flat value
+      } else if ($fieldsLvl == $tag['level'] && 'open' == $tag['type']) {// field - nested - start
+        $lastIdxToSplice = $i;
+        $tagsToSplice[$lastIdxToSplice] = $tag;
+        $tagsToSplice[$lastIdxToSplice]['action'] = 'Insert -- TODO: L.' . __LINE__;
+        $tagsToSplice[$lastIdxToSplice]['tags'] = array();
+        $tagsToSplice[$lastIdxToSplice]['tags'][] = $tag;
+      } else if (null !== $lastIdxToSplice && $fieldsLvl == $tag['level'] && 'close' == $tag['type']) {// field - nested - end
+        $tagsToSplice[$lastIdxToSplice]['tags'][] = $tag;
+        $lastIdxToSplice = null;
+      } else if (null !== $lastIdxToSplice) {
+        $tagsToSplice[$lastIdxToSplice]['tags'][] = $tag;
+      } else {
+        if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " BUG: Update to flat fields loop({$i}) \$tag('{$tag['tag']}', '{$tag['type']}', '{$tag['level']}')"."\n";}
+        throw new Exception("Error Processing Update Request at Update tag for type '{$action['typeName']}'");
+      }
+    }
+
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . ' TODO: FIX $action:';print_r($action);echo "\n";}
+    if (empty($tagsToSplice)) throw new Exception("Error Processing Update Request - missing Property or Filter tags");
+    if (!empty($tagsToSplice)) {// Update
+      // if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Insert to flat fields \$tagsToSplice:";print_r($tagsToSplice);echo "\n";}
+      $tagsToSplice = array_reverse($tagsToSplice, $preserve_keys = true);
+      foreach ($tagsToSplice as $offset => $childTag) {
+        $toRemove = count($childTag['tags']);
+        array_pop($childTag['tags']);// remove last closing tag
+        $tag = array_shift($childTag['tags']);
+        $childTag['typeName'] = $action['typeName'];
+        $childTag['action'] = 'Update -- TODO L.' . __LINE__;
+        array_splice($action['tags'], $offset, $toRemove, array($childTag));
+      }
+      // if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Insert to flat fields \$action:";print_r($action);echo "\n";}
+    }
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Update action after first split \$action:";print_r($action);echo "\n";}
+    if (count($action['tags']) < 2) throw new Exception("Error Processing Update Request - missing Property or Filter tags");
+    $filterTag = array_pop($action['tags']);
+    $action['Filter'] = $this->validateUpdateFilterTag($filterTag);
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Update action after validate Filter \$action['Filter']:";print_r($action['Filter']);echo "\n";}
+    $action['fields'] = $this->validateUpdatePropertyTags($action['tags']);
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Update action after validate Property \$action['fields']:";print_r($action['fields']);echo "\n";}
+    foreach ($action['fields'] as $fieldName => $propertyTag) {
+      if (!$this->validateFieldAction($fieldName, 'W')) throw new Api_WfsException("Access Denied to Update field '{$fieldName}' in object '{$action['typeName']}'", __LINE__, null, 'MissingFieldPermWrite', 'request');
+      $value = $propertyTag['value'];
+      $this->validateFieldRestrictions($fieldName, $value);
+    }
+
+    return $action;
+  }
+
+  public function validateUpdateFilterTag($filterTag) {// @returns Filter tag (feature id), throws Exceptions
+    // TODO: allow ogc filter for update multiple rows
+    $DBG = V::get('DBG_XML', 0, $_GET, 'int');
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Update validateUpdateFilterTag \$filterTag:";print_r($filterTag);echo "\n";}
+    if ('Filter' != $filterTag['tag']) throw new Exception("Error Processing Update Request - missing Filter tag");
+    $idFeature = null;
+    if (empty($filterTag['tags'])) throw new Exception("Error Processing Update Request - wrong Filter tag format");
+    if ('FeatureId' != $filterTag['tags'][0]['tag']) throw new Exception("Error Processing Update Request - wrong Filter tag format");
+    if (empty($filterTag['tags'][0]['attributes']['fid'])) throw new Exception("Error Processing Update Request - missing Filter tag @fid");
+    $idFeature = $filterTag['tags'][0]['attributes']['fid'];
+    $featureName = $this->getName();
+    if ("{$featureName}." != substr($idFeature, 0, strlen("{$featureName}."))) throw new Exception("Error Processing Update Request - wrong typeName in Filter/@fid");
+    $idFeature = substr($idFeature, strlen("{$featureName}."));
+    if (!$this->checkPrimaryKeyFormat($idFeature)) throw new Exception("Error Processing Update Request - wrong primary key format in Filter/@fid");
+    return $idFeature;
+  }
+
+  public function checkPrimaryKeyFormat($idFeature) {// @returns bool
+    return ((string)$idFeature === (string)intval($idFeature));
+  }
+
+  public function validateUpdatePropertyTags($tags) {// @returns Property tags (fields), throws Exceptions
+    $DBG = V::get('DBG_XML', 0, $_GET, 'int');
+    $fields = array();
+    if($DBG>3){echo 'C.'.get_class($this).' L.' . __LINE__ . " Update validateUpdatePropertyTags \$fields:";print_r($tags);echo "\n";}
+    foreach ($tags as $idx => $tag) {
+      if ('Property' != $tag['tag']) throw new Exception("Error Processing Update Request - tag '{$tag['tag']}' is not allowed in Update tag");
+      if (count($tag['tags']) < 2) throw new Exception("Error Processing Update Request - wrong format in Update/Property[{$idx}]");
+      $tagName = array_shift($tag['tags']);
+      if ('Name' != $tagName['tag']) throw new Exception("Error Processing Update Request - missing Name tag in Update/Property[{$idx}]");
+      if (empty($tagName['value'])) throw new Exception("Error Processing Update Request - empty field name in Update/Property[{$idx}]");
+      if (false !== strpos($tagName['value'], '/')) throw new Exception("Error Processing Update Request - xpath in Update/Property[{$idx}] field name not implemented", 501);
+      // TODO: check field name as xpath, eg. File/content
+      $fieldName = $tagName['value'];
+
+      $tagValue = array_shift($tag['tags']);
+      $fieldType = $this->getXsdFieldType($fieldName);
+      if ('Value' != $tagValue['tag']) throw new Exception("Error Processing Update Request - missing Value tag in Update/Property[{$idx}]");
+      if ('open' == $tagValue['type']) {
+        array_pop($tag['tags']);
+        $tagValue['tags'] = $tag['tags'];
+        if (empty($tagValue['tags'])) throw new Exception("Error Processing Update Request - wrong Value tag format in Update/Property[{$idx}]");
+        if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " F.".__FUNCTION__." validate fields loop({$idx}) loop({$fieldName}) \$fieldType({$fieldType})"."\n";}
+        if ('gml:' == substr($fieldType, 0, 4)) {
+          $tagValue['value'] = $this->convertGmlTagsToWkt($fieldType, $tagValue['tags']);
+        } else {
+          // TODO: return $fields[$fieldName][] = array('type'=>'open', 'typeName'=>$fieldType, 'tags'=>...
+          throw new Exception("Error Processing Update Request - wrong Value tag format for field '{$fieldName}' (Update/Property[{$idx}])");
+        }
+      } else if ('complete' == $tagValue['type']) {
+        if (!empty($tag['tags'])) throw new Exception("Error Processing Update Request - wrong Value tag format in Update/Property[{$idx}]");
+      } else throw new Exception("Error Processing Update Request - missing Value tag in Update/Property[{$idx}]");
+
+      $value = V::get('value', '', $tagValue);
+      $fields[$fieldName][] = array('type'=>'complete', 'value'=>$value);
+    }
+    return $fields;
+  }
+
+  public function validateInsertXml($action) {// @returns action tag, throws Exceptions
+    $DBG = V::get('DBG_XML', 0, $_GET, 'int');
+    // if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " TODO: F.".__FUNCTION__." \$action:";print_r($action);echo "\n";}
+    // split Insert tags by first level fields - nested structures like gml (the_geom)
+    $fieldsLvl = $action['tags'][0]['level'];
+    $totalTags = count($action['tags']);
+    $tagsToSplice = array();// args for splice(offset, length to remove, new values)
+    for ($i = 0, $cnt = $totalTags, $lastIdxToSplice = 0; $i < $cnt; $i++) {
+      $tag = $action['tags'][$i];
+      if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Insert to flat fields loop({$i}) \$lastIdxToSplice({$lastIdxToSplice}) \$tag('{$tag['tag']}', '{$tag['type']}', '{$tag['level']}')"."\n";}
+      if ($fieldsLvl == $tag['level'] && 'complete' == $tag['type']) {// field - flat value
+      } else if ($fieldsLvl == $tag['level'] && 'open' == $tag['type']) {// field - nested - start
+        $lastIdxToSplice = $i;
+        $tagsToSplice[$lastIdxToSplice] = $tag;
+        $tagsToSplice[$lastIdxToSplice]['action'] = 'Insert';
+        $tagsToSplice[$lastIdxToSplice]['tags'] = array();
+        $tagsToSplice[$lastIdxToSplice]['tags'][] = $tag;
+      } else if (null !== $lastIdxToSplice && $fieldsLvl == $tag['level'] && 'close' == $tag['type']) {// field - nested - end
+        $tagsToSplice[$lastIdxToSplice]['tags'][] = $tag;
+        $lastIdxToSplice = null;
+      } else if (null !== $lastIdxToSplice) {
+        $tagsToSplice[$lastIdxToSplice]['tags'][] = $tag;
+      } else {
+        if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " BUG: Insert to flat fields loop({$i}) \$tag('{$tag['tag']}', '{$tag['type']}', '{$tag['level']}')"."\n";}
+        throw new Exception("Error Processing Request at Insert tag for type '{$action['typeName']}'");
+      }
+    }
+
+    // if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . ' TODO: FIX $action:';print_r($action);echo "\n";}
+    if (!empty($tagsToSplice)) {// Insert deep object
+      // if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Insert to flat fields \$tagsToSplice:";print_r($tagsToSplice);echo "\n";}
+      $tagsToSplice = array_reverse($tagsToSplice, $preserve_keys = true);
+      foreach ($tagsToSplice as $offset => $childTag) {
+        $toRemove = count($childTag['tags']);
+        array_pop($childTag['tags']);// remove last closing tag
+        $tag = array_shift($childTag['tags']);
+        $childTag['action'] = 'Insert';
+        {// TODO: duplicate code - get prefix for typeName
+          $typeName = $tag['tag'];
+          if (false === strpos($typeName, ':')) {
+            $nsType = V::get('xmlns', '', $tag['attributes']);
+            if (!$nsType) throw new Exception("Error Processing Request - Missing object namespace '{$tag['tag']}'");
+            $prefix = Api_WfsNs::getNsPrefix($nsType);
+            if (!$prefix) {
+              if ($typeName == substr(rtrim($nsType, '/'), -1 * strlen($typeName))) {// typeName may be added to ns uri
+                $nsBaseForType = substr(rtrim($nsType, '/'), 0, -1 * strlen($typeName) - 1);
+                $prefix = Api_WfsNs::getNsPrefix($nsBaseForType);
+              }
+            }
+            if (!$prefix) throw new Exception("Error Processing Request - Unrecognized namespace uri '{$nsType}' for object '{$tag['tag']}'");
+            $typeName = "{$prefix}:{$typeName}";
+          }
+          $childTag['typeName'] = $typeName;
+        }
+        array_splice($action['tags'], $offset, $toRemove, array($childTag));
+        $action['isDeepObject'] = true;
+      }
+      // if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " Insert to flat fields \$action:";print_r($action);echo "\n";}
+    }
+
+    // TODO: validate sequence order from schema - needed?
+
+    // validate fields
+    $action['fields'] = array();
+    foreach ($action['tags'] as $idx => $tag) {
+      $fieldName = $tag['tag'];
+      if (false !== strpos($fieldName, ':')) $fieldName = substr($fieldName, strpos($fieldName, ':') + 1);
+      if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " F.".__FUNCTION__." validate fields loop({$idx}) \$fieldName:";print_r($fieldName);echo "\n";}
+      $field = array();
+      $field['tag'] = $tag['tag'];
+      $field['type'] = $tag['type'];
+      if (!empty($tag['typeName'])) $field['typeName'] = $tag['typeName'];
+      if (!empty($tag['action'])) $field['action'] = $tag['action'];
+      if (!empty($tag['value'])) $field['value'] = $tag['value'];
+      if (!empty($tag['tags'])) $field['tags'] = $tag['tags'];
+      $action['fields'][$fieldName][] = $field;
+    }
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " F.".__FUNCTION__." validate fields loop({$idx}) \$action['fields']:";print_r($action['fields']);echo "\n";}
+    foreach ($action['fields'] as $fieldName => $childFields) {
+      foreach ($childFields as $idx => $field) {
+        // TODO: validate minOccurs, maxOccurs
+      }
+    }
+    $fieldPK = $this->getPrimaryKeyField();
+    foreach ($action['fields'] as $fieldName => $childFields) {
+      if ($fieldName == $fieldPK) continue;
+      foreach ($childFields as $idx => $field) {
+        if (!$this->validateFieldAction($fieldName, 'C')) throw new Api_WfsException("Access Denied to Create field '{$fieldName}' in object '{$action['typeName']}'", __LINE__, null, 'MissingFieldPermCreate', 'request');
+        if ('open' == $field['type']) {
+          $fieldType = $this->getXsdFieldType($fieldName);
+          if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " F.".__FUNCTION__." validate fields loop({$idx}) loop({$fieldName}) \$fieldType({$fieldType})"."\n";}
+          if ('gml:' == substr($fieldType, 0, 4)) {
+            $action['fields'][$fieldName][$idx]['value'] = $this->convertGmlTagsToWkt($fieldType, $field['tags']);
+            unset($action['fields'][$fieldName][$idx]['tags']);
+            unset($action['fields'][$fieldName][$idx]['typeName']);
+            unset($action['fields'][$fieldName][$idx]['action']);
+            $action['fields'][$fieldName][$idx]['type'] = 'complete';
+          } else if ('p5_objects:' == substr($fieldType, 0, 11)) {
+            // TODO: read value recursive? (like geom?)
+          } else if ('ref:p5_objects:' == substr($fieldType, 0, 15)) {
+            // TODO: read value recursive? - validate recursive
+          }
+        }
+        $this->validateFieldRestrictions($fieldName, $value);
+      }
+    }
+    // TODO: validate nillable
+
+    // TODO: validate recursive fields with type 'p5_objects:*' (without 'value')
+    foreach ($action['fields'] as $fieldName => $childFields) {
+      foreach ($childFields as $idx => $field) {
+        if ('open' == $field['type']) {
+          $fieldType = $this->getXsdFieldType($fieldName);
+          if ('ref:p5_objects:' == substr($fieldType, 0, 15)) {
+            if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " F.".__FUNCTION__." TODO: -------> validateInsertXml(\$field) \$field:";print_r($field);echo "\n";}
+            $acl = $this->getAclFromTypeName(substr($fieldType, 4));
+            $action['fields'][$fieldName][$idx] = $acl->validateInsertXml($field);
+          }
+        }
+      }
+    }
+
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " F.".__FUNCTION__." return ============ \$action:";print_r($action);echo "\n";}
+    return $action;
+  }
+
+  public function validateFieldRestrictions($fieldName, $value) {// @returns null, throws Exceptions
+    // TODO: restrictions
+  }
+
+  public function checkFieldRestrictions($fieldName, $value) {// @returns array of error msgs
+    // TODO: restrictions
+  }
+
+  // like Api_WfsGeomTypeConverter::convertGmlCoordinatesToWkt($gmlCoordinates) where $gmlCoordinates is from converted wfs request
+  public function convertGmlTagsToWkt($fieldType, $tags) {
+    $DBG = V::get('DBG_XML', 0, $_GET, 'int');
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " TODO: F.".__FUNCTION__." \$fieldType({$fieldType}) \$tags:";print_r($tags);echo "\n";}
+
+    $cs = null; $ts = null; $value = null; $wktType = null;
+    if ('gml:LineStringPropertyType' == $fieldType) {
+      // <gml:LineString srsName="EPSG:4326">
+      //   <gml:coordinates cs="," ts=" ">18.25240580856418049,54.48879768607960017 18.27014261382555915,54.46219247818753217</gml:coordinates>
+      // </gml:LineString>
+      if (3 != count($tags)) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:LineString'");
+      if ('gml:LineString' !== $tags[0]['tag']) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:LineString'");
+      if ('open' !== $tags[0]['type']) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:LineString'");
+      if (empty($tags[1]['attributes']['cs'])) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:LineString'");
+      if (empty($tags[1]['attributes']['ts'])) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:LineString'");
+      if (empty($tags[1]['value'])) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:LineString'");
+      $cs = $tags[1]['attributes']['cs'];
+      $ts = $tags[1]['attributes']['ts'];
+      $value = $tags[1]['value'];
+      $wktType = 'LINESTRING';
+    } else if ('gml:PointPropertyType' == $fieldType) {
+      // <gml:Point srsDimension="1" srsName="http://www.opengis.net/gml/srs/epsg.xml#3857">
+      //  <gml:coordinates decimal="." cs="," ts=" ">-33.7291,-56.3353820</gml:coordinates>
+      // </gml:Point>
+      // TODO: @decimal
+      if (3 != count($tags)) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Point'");
+      if ('gml:Point' !== $tags[0]['tag']) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Point'");
+      if ('open' !== $tags[0]['type']) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Point'");
+      if (empty($tags[1]['attributes']['cs'])) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Point'");
+      if (empty($tags[1]['attributes']['ts'])) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Point'");
+      if (empty($tags[1]['value'])) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Point'");
+      $cs = $tags[1]['attributes']['cs'];
+      $ts = $tags[1]['attributes']['ts'];
+      $value = $tags[1]['value'];
+      $wktType = 'POINT';
+    } else if ('gml:PolygonPropertyType' == $fieldType) {
+      if (7 != count($tags)) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Polygon'");
+      if ('gml:Polygon' !== $tags[0]['tag']) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Polygon'");
+      if ('open' !== $tags[0]['type']) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Polygon'");
+      if ('gml:outerBoundaryIs' !== $tags[1]['tag']) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Polygon'");
+      if ('gml:LinearRing' !== $tags[2]['tag']) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Polygon'");
+      if ('gml:coordinates' !== $tags[3]['tag']) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Polygon'");
+      if (empty($tags[3]['attributes']['cs'])) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Polygon'");
+      if (empty($tags[3]['attributes']['ts'])) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Polygon'");
+      if (empty($tags[3]['value'])) throw new Exception("#L".__LINE__." Error Processing Request - expected type 'gml:Polygon'");
+      $cs = $tags[3]['attributes']['cs'];
+      $ts = $tags[3]['attributes']['ts'];
+      $value = $tags[3]['value'];
+      $wktType = 'POLYGON';
+    } else {
+      throw new Exception("Error Processing Request - type '{$fieldType}' not supported");
+    }
+
+    $wkt = $value;
+    $wkt = str_replace(array($cs, $ts), array('#cs#', '#ts#'), $wkt);
+    $wkt = str_replace(array('#cs#', '#ts#'), array(' ', ','), $wkt);
+    if ('POLYGON' == $wktType) $wkt = "({$wkt})";// POINT(1 1), LINESTRING(0 0,1 1,2 2), POLYGON((0 0,10 0,10 10,0 10,0 0))
+    return "{$wktType}({$wkt})";
+  }
+
+  public function getAclFromTypeName($typeName) {// TODO: mv to another class, duplicate from Api_WfsServerBase::getAclFromTypeName($typeName)
+		$typeEx = explode(':', $typeName);
+		if (2 != count($typeEx)) throw new Api_WfsException("Could not get acl for '{$typeName}' - syntax error");
+		if ('p5_' != substr($typeEx[0], 0, 3)) throw new Api_WfsException("Could not get acl for '{$typeName}' - prefix error");
+		$sourceName = substr($typeEx[0], 3);
+		$objName = $typeEx[1];
+		$acl = User::getAcl()->getObjectAcl($sourceName, $objName);
+		if (!$acl) throw new Api_WfsException("Could not get acl for '{$typeName}'");
+		$forceTblAclInit = 0;//('1' == V::get('_force', '', $_GET));
+		$acl->init($forceTblAclInit);
+		return $acl;
+	}
+
+  public function insertXml($action) {// @param $action = [ 'typeName' => '*:*', 'fields'=>[ `name`=>[`fld`, `fld`, ...], ... ] ]
+    $DBG = V::get('DBG_XML', 0, $_GET, 'int');
+    if (empty($action['typeName'])) throw new Exception("Error Processing Request - missing typeName", 500);
+    if (empty($action['fields'])) throw new Exception("Error Processing Request - missing fields for type '{$action['typeName']}'", 500);
+    {// Insert with primaryKey -> Update
+      $fieldPK = $this->getPrimaryKeyField();
+      if (array_key_exists($fieldPK, $action['fields'])) {
+        $pk = $action['fields'][$fieldPK][0]['value'];
+        $action['Filter'] = $pk;
+        $affected = $this->updateXml($action);
+        return ($affected >= 0) ? $pk : -1;
+      }
+    }
+    $sqlInsert = array();
+    $sqlChildInsert = array();
+    foreach ($action['fields'] as $fieldName => $childFields) {
+      foreach ($childFields as $idx => $field) {
+        if ('complete' == $field['type']) {
+          $sqlInsert[$fieldName][$idx] = $field['value'];
+        } else if ('open' == $field['type']) {
+          $childAcl = $this->getAclFromTypeName($field['typeName']);// TODO: or $fieldType = $this->getXsdFieldType($fieldName);// TODO: ref:p5_objects:File
+          $sqlChildInsert[$fieldName][$idx] = $childAcl->insertXml($field);
+        } else throw new Exception("Error Processing Request - BUG Unrecognized type '{$field['type']}'", 500);
+      }
+    }
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " F.".__FUNCTION__." \$sqlInsert:";print_r($sqlInsert);echo "\n";}
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " F.".__FUNCTION__." \$sqlChildInsert:";print_r($sqlChildInsert);echo "\n";}
+
+    $sqlObj = array();
+    foreach ($sqlInsert as $fieldName => $childFields) {
+      $value = $childFields[0];
+      // foreach ($childFields as $idx => $value) // TODO: check maxOcures
+      if ($this->isGeomField($fieldName)) $value = "GeomFromText('{$value}')";
+      $sqlObj["{$fieldName}"] = $value;
+    }
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " F.".__FUNCTION__." addItem(\$sqlObj) \$sqlObj:";print_r($sqlObj);echo "\n";}
+    $insertedId = $this->addItem($sqlObj);
+
+    foreach ($sqlChildInsert as $fieldName => $childFields) {
+      foreach ($childFields as $idx => $childInsertedId) {
+        $childAcl = $this->getAclFromTypeName($action['fields'][$fieldName][$idx]['typeName']);// TODO: or $fieldType = $this->getXsdFieldType($fieldName);// TODO: ref:p5_objects:File
+        $this->_insertRef($this->getRootTableName(), $childAcl->getRootTableName(), $insertedId, $childInsertedId);
+      }
+    }
+
+    return $insertedId;
+  }
+
+  public function _insertRef($tableName, $childTableName, $pk, $childPk) {// TODO: $idTransaction
+    $refTable = $this->createRefTable($tableName, $childTableName);
+    $sqlPk = DB::getPDO()->quote($pk, PDO::PARAM_STR);
+    $sqlChildPk = DB::getPDO()->quote($childPk, PDO::PARAM_STR);
+    DB::getPDO()->exec("
+      insert into `{$refTable}` (`PRIMARY_KEY`, `REMOTE_PRIMARY_KEY`)
+      values ({$sqlPk}, {$sqlChildPk})
+    ");
+  }
+
+  public function createRefTable($tableName, $childTableName) {// TODO: $idTransaction
+    static $tables = array();
+    $refTable = "{$tableName}__#REF__{$childTableName}";
+    if (array_key_exists($refTable, $tables)) return $refTable;
+    DB::getPDO()->exec("
+      CREATE TABLE IF NOT EXISTS `{$refTable}` (
+        `PRIMARY_KEY` int(11) NOT NULL,
+        `REMOTE_PRIMARY_KEY` int(11) NOT NULL,
+        -- TODO `TRANACTION_ID` int(11) NOT NULL
+        KEY `PRIMARY_KEY` (`PRIMARY_KEY`),
+        KEY `REMOTE_PRIMARY_KEY` (`REMOTE_PRIMARY_KEY`)
+      ) ENGINE=MyISAM DEFAULT CHARSET=latin2;
+    ");
+    $tables[$refTable] = true;
+    return $refTable;
+  }
+
+  public function updateXml($action) {// @param $action = [ 'typeName' => '*:*', 'Filter'=>int, 'fields'=>[ `name`=>[`fld`, `fld`, ...], ... ] ]
+    $DBG = V::get('DBG_XML', 0, $_GET, 'int');
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " updateXml action \$action:";print_r($action);echo "\n";}
+    if (empty($action['typeName'])) throw new Exception("Error Processing Request - missing typeName", 500);
+    if (empty($action['Filter'])) throw new Exception("Error Processing Request - missing Filter for type '{$action['typeName']}'", 500);
+    if (!$this->checkPrimaryKeyFormat($action['Filter'])) throw new Exception("Error Processing Request - wrong Filter format for type '{$action['typeName']}'", 501);
+    if (empty($action['fields'])) throw new Exception("Error Processing Request - missing Property for type '{$action['typeName']}'", 500);
+    // TODO: check acl user to update record ID = $action['Filter']
+    $itemPatch = array();
+    foreach ($action['fields'] as $fieldName => $childFields) {
+      foreach ($childFields as $idx => $field) {
+        if ('complete' != $field['type']) continue;// skip child nodes, REF
+        $value = $field['value'];
+        if ($this->isGeomField($fieldName)) $value = "GeomFromText('{$value}')";
+        $itemPatch[$fieldName] = $value;
+      }
+    }
+    $itemPatch[$this->getPrimaryKeyField()] = $action['Filter'];
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " updateXml action \$itemPatch:";print_r($itemPatch);echo "\n";}
+    $affected = $this->updateItem($itemPatch);
+    // TODO: update/insert child nodes, REF
+    return $affected;
+  }
+
+  public function deleteXml($action) {// @param $action = [ 'typeName' => '*:*', 'Filter'=>[int, ...], 'fields'=>[ `name`=>[`fld`, `fld`, ...], ... ] ]
+    $DBG = V::get('DBG_XML', 0, $_GET, 'int');
+    if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " updateXml action \$action:";print_r($action);echo "\n";}
+    if (empty($action['typeName'])) throw new Exception("Error Processing Request - missing typeName", 500);
+    if (!is_array($action['Filter']) || empty($action['Filter'])) throw new Exception("Error Processing Request - missing Filter for type '{$action['typeName']}'", 500);
+    foreach ($action['Filter'] as $fid) {
+      if (!$this->checkPrimaryKeyFormat($fid)) throw new Exception("Error Processing Request - wrong Filter format for type '{$action['typeName']}'", 501);
+    }
+    if (empty($action['fields'])) throw new Exception("Error Processing Request - missing Property for type '{$action['typeName']}'", 500);
+    // TODO: check acl user to update record ID = $action['Filter']
+    $itemPatch = array();
+    foreach ($action['fields'] as $fieldName => $childFields) {
+      foreach ($childFields as $idx => $field) {
+        if ('complete' != $field['type']) continue;// skip child nodes, REF
+        $value = $field['value'];
+        if ($this->isGeomField($fieldName)) $value = "GeomFromText('{$value}')";
+        $itemPatch[$fieldName] = $value;
+      }
+    }
+    $fieldPK = $this->getPrimaryKeyField();
+    $deleted = 0;
+    foreach ($action['Filter'] as $fid) {
+      $itemPatch[$fieldPK] = $fid;
+      if($DBG){echo 'C.'.get_class($this).' L.' . __LINE__ . " updateXml action \$itemPatch:";print_r($itemPatch);echo "\n";}
+      $affected = $this->updateItem($itemPatch);
+      if ($affected >= 0) $deleted += 1;
+    }
+    // TODO: update/insert child nodes, REF
+    return $deleted;
+  }
 
 }

+ 25 - 15
SE/se-lib/FileStorageAcl.php

@@ -10,7 +10,8 @@ class FileStorageAcl extends Core_AclBase {
   public function init($force = false) {}
   public function isInitialized() { return true; }
   public function getName() { return 'File'; }
-  public function getRealFieldListByIdZasob($force = false) {
+  public function getRootTableName() { return 'CRM_FILES'; }
+  public function getRealFieldListByIdZasob() {
     $cols = array();// FileStorage::getFileById()
     $cols[1] = 'id';
     $cols[2] = 'name';
@@ -31,27 +32,21 @@ class FileStorageAcl extends Core_AclBase {
 		}
 		return null;
 	}
-  public function isIntegerField($fieldName) {
-    if ('id' == $fieldName) return true;
-    if ('size' == $fieldName) return true;
-    if ('version' == $fieldName) return true;
-    return false;
-  }
   public function isDecimalField($fieldName) { return false; }
-	public function isGeomField($fldName) { return false; }
-	public function isDateField($fldName) { return false; }
-	public function isDateTimeField($fldName) { return false; }
+	public function isGeomField($fieldName) { return false; }
+	public function isDateField($fieldName) { return false; }
+	public function isDateTimeField($fieldName) { return false; }
 	public function isStringField($fieldName) {
     if ('name' == $fieldName) return true;
     if ('mimeType' == $fieldName) return true;
     return false;
   }
-	public function isTextField($fldName) { return false; }
+	public function isTextField($fieldName) { return false; }
   public function isBinaryField($fieldName) {
     if ('content' == $fieldName) return true;
     return false;
   }
-	public function isEnumerationField($fldName) { return false; }
+	public function isEnumerationField($fieldName) { return false; }
   public function getFieldType($colName) {
     switch ($colName) {
       case 'id': return array(); break;
@@ -85,8 +80,8 @@ class FileStorageAcl extends Core_AclBase {
       $parser->loadOgcFilter($ogcFilter);
       $queryWhereBuilder = $parser->convertToSqlQueryWhereBuilder();
       $usedFields = $queryWhereBuilder->getUsedFields();
-      foreach ($usedFields as $fldName) {
-        if (!$this->getFieldIdByName($fldName)) throw new Exception("Not allowed PropertyName '{$fldName}'");
+      foreach ($usedFields as $fieldName) {
+        if (!$this->getFieldIdByName($fieldName)) throw new Exception("Not allowed PropertyName '{$fieldName}'");
       }
       $sqlWhereAddOgcFilter = $queryWhereBuilder->getQueryWhere('t');
       if (!empty($sqlWhereAddOgcFilter)) $sqlWhereAddOgcFilter = " and {$sqlWhereAddOgcFilter}";
@@ -154,7 +149,7 @@ class FileStorageAcl extends Core_AclBase {
     return FileStorage::addFile($binaryContent, $fileName);
   }
 
-  public function getGeomFieldType() { return null; }
+  public function getGeomFieldType($fieldName) { return null; }
   public function getPrimaryKeyField() { return 'id'; }
   public function getID() { return 0; }
   public function getAttributesFromZasoby() {
@@ -164,5 +159,20 @@ class FileStorageAcl extends Core_AclBase {
     // if (!$acl->hasFieldPerm($idZasob, 'R')) $elNode->setAttributeNS($rootWfsNsUri, "{$rootWfsNs}:allow_read", "false");
 		return $attributes;
 	}
+  public function validateFieldAction($fieldName, $perms, $record = null) {
+    if ($this->getXsdFieldType($fieldName)) return true;
+    return false;
+	}
+  public function getXsdFieldType($fieldName) {
+    switch ($fieldName) {
+      case 'id': return 'xsd:integer';
+      case 'name': return 'xsd:string';
+      case 'size': return 'xsd:integer';
+      case 'mimeType': return 'xsd:string';
+      case 'version': return 'xsd:integer';
+      case 'content': return 'xsd:base64Binary';
+      default: throw new HttpException("Error field not exists '{$fieldName}'", 404);
+    }
+  }
 
 }

+ 13 - 1
SE/se-lib/Request.php

@@ -16,6 +16,18 @@ class Request {
 		return false;
 	}
 
+	// @usage: Request::getHostUri();
+	public static function getHostUri() {
+		// return current host with protocol
+		// [SCRIPT_URI] => http://biuro.biall-net.pl:34543/dev-pl/se-master/wfs-qgis.php/default_db/
+		// [SCRIPT_URL] => /dev-pl/se-master/wfs-qgis.php/default_db/
+		// [SCRIPT_NAME] => /dev-pl/se-master/wfs-qgis.php
+		// [HTTP_HOST] => biuro.biall-net.pl
+		$uri = (Request::isHttps())? 'https://' : 'http://';
+		$uri .= "{$_SERVER['HTTP_HOST']}";
+		return $uri;
+	}
+
 	// @usage: Request::getScriptUri();
 	public static function getScriptUri() {
 		// [SCRIPT_URI] => http://biuro.biall-net.pl:34543/dev-pl/se-master/wfs-qgis.php/default_db/
@@ -27,7 +39,7 @@ class Request {
 		return $uri;
 	}
 
-	// @usage: Request::getPathUri();
+	// @usage: Request::getPathUri() . "indeks.php?..."
 	public static function getPathUri() {
 		// [SCRIPT_URI] => http://biuro.biall-net.pl:34543/dev-pl/se-master/wfs-qgis.php/default_db/
 		// [SCRIPT_URL] => /dev-pl/se-master/wfs-qgis.php/default_db/

+ 24 - 1
SE/se-lib/TableAcl.php

@@ -39,6 +39,7 @@ class TableAcl extends Core_AclBase {
 	public function __construct($zasobID) { $this->_zasobID = $zasobID; }
 	public function getSourceName() { return 'default_db'; }
 	public function getName() { return $this->_name; }
+	public function getRootTableName() { return $this->_name; }
 	public function getID() { return $this->_zasobID; }
 	public function setName($name) { $this->_name = $name; }
 	public function setOpis($opis) { $this->_opis = $opis; }
@@ -286,6 +287,9 @@ class TableAcl extends Core_AclBase {
 	/**
 	 * @param $taskPerm - 'C', 'W', 'R'
 	 */
+	public function validateFieldAction($fieldName, $perms, $record = null) {
+		return $this->isAllowed($fieldID = $this->getFieldIdByName($fieldName), $perms, $record);
+	}
 	public function isAllowed($fieldID, $taskPerm, $record = null) {
 		if (!in_array($taskPerm, array('C', 'W', 'R'))) {
 			return false;
@@ -1326,7 +1330,7 @@ class TableAcl extends Core_AclBase {
 		return $ds->getItem($id);
 	}
 
-	public function getItems($params) {
+	public function getItems($params = array()) {
 		$ds = $this->getDataSource();
 		return $ds->getItems($params);
 	}
@@ -1716,4 +1720,23 @@ class TableAcl extends Core_AclBase {
 		return $attributes;
 	}
 
+	public function getXsdFieldType($fldName) {// @returns string - xsd type, TODO: throw Exception when field not found
+		$fldType = 'xsd:string';
+		if ($this->isIntegerField($fldName)) return 'xsd:integer';
+		else if ($this->isDecimalField($fldName)) return 'xsd:decimal';
+		else if ($this->isDateField($fldName)) return 'xsd:date';
+		else if ($this->isDateTimeField($fldName)) return 'xsd:dateTime';
+		else if ($this->isGeomField($fldName)) {
+			//$fldType = 'gml:GeometryPropertyType';
+			$geomType = $this->getGeomFieldType($fldName);
+			if ('polygon' == $geomType) return 'gml:PolygonPropertyType';
+			else if ('point' == $geomType) return 'gml:PointPropertyType';
+			else if ('linestring' == $geomType) return 'gml:LineStringPropertyType';
+			else return 'gml:GeometryPropertyType';
+		}
+		else if ($this->isEnumerationField($fldName)) return 'xsd:string';
+		else if ($this->isBinaryField($fldName)) return 'xsd:base64Binary';
+		return $fldType;
+	}
+
 }

+ 125 - 0
SE/se-lib/TestPermsStorageAcl.php

@@ -0,0 +1,125 @@
+<?php
+
+Lib::loadClass('Core_AclBase');
+Lib::loadClass('FileStorage');
+
+class TestPermsStorageAcl extends Core_AclBase {
+
+  public function __construct() {
+    $this->parentAcl = User::getAcl()->getObjectAcl('default_db', 'TEST_PERMS');
+    // DBG::_(true, true, "parentAcl", $this->parentAcl, __CLASS__, __FUNCTION__, __LINE__);
+  }
+  public function getSourceName() { return 'objects'; }
+  public function init($force = false) {}
+  public function isInitialized() { return true; }
+  public function getName() { return 'TestPerms'; }
+  public function getRootTableName() { return 'TEST_PREMS'; }
+  public function getRealFieldListByIdZasob($force = false) {
+    $cols = $this->parentAcl->getRealFieldListByIdZasob();
+    foreach ($cols as $id => $name) {
+      if ('ID' == $name) $cols[$id] = 'id';
+    }
+    $cols[100000] = 'File';
+    return $cols;
+  }
+  public function getFieldIdByName($fieldName) {
+    $fields = $this->getRealFieldListByIdZasob();
+		if (empty($fieldName)) return null;
+		foreach ($fields as $idField => $vFieldName) {
+			if ($vFieldName == $fieldName) return $idField;
+		}
+		return null;
+	}
+  public function getFieldType($colName) {
+    switch ($colName) {
+      case 'id': return array(); break;
+    }
+    return null;
+	}
+  public function isAllowed($idZasob, $taskPerm, $record = null) {
+    if ('C' == $taskPerm && $idZasob > 1 && $idZasob < 7) return true;
+    if ('R' == $taskPerm && $idZasob > 0 && $idZasob < 7) return true;
+    return false;
+  }
+  public function hasFieldPerm($idZasob, $taskPerm) {
+    if ('C' == $taskPerm && $idZasob > 1 && $idZasob < 7) return true;
+    if ('R' == $taskPerm && $idZasob > 0 && $idZasob < 7) return true;
+    return false;
+  }
+  public function getItems($params = array()) {
+    $DBG = V::get('DBG_DS', 0, $_GET, 'int');
+    if($DBG>2){echo 'C.'.get_class($this).' L.' . __LINE__ . " getItems \$params:";print_r($params);echo "\n";}
+    if (!empty($params['ogc:Filter'])) $params['ogc:Filter'] = str_replace('<ogc:PropertyName>id</ogc:PropertyName>', '<ogc:PropertyName>ID</ogc:PropertyName>', $params['ogc:Filter']);
+    $items = array();
+    $rawItems = $this->parentAcl->getItems($params);
+    foreach ($rawItems as $pk => $item) $items[$pk] = (array)$item;
+    if($DBG>2){echo 'C.'.get_class($this).' L.' . __LINE__ . " getItems \$items:";print_r($items);echo "\n";}
+    if (empty($items)) return $items;
+    $this->fetchItemRef($items);
+    if($DBG>2){echo 'C.'.get_class($this).' L.' . __LINE__ . " getItems after fetchItemRef \$items:";print_r($items);echo "\n";}
+    return $items;
+  }
+  public function fetchItemRef(&$items) {
+    $DBG = V::get('DBG_DS', 0, $_GET, 'int');
+    $refs = array();// fieldName => xsdType
+    foreach ($this->getRealFieldListByIdZasob() as $id => $fieldName) {
+      $fieldType = $this->getXsdFieldType($fieldName);
+      if ('ref:' == substr($fieldType, 0, 4)) $refs[$fieldName] = substr($fieldType, 4);
+    }
+    if (empty($refs)) return $items;
+    $fidList = array_keys($items);
+    $sqlPk = array(); foreach ($fidList as $pk) { $sqlPk[] = "'{$pk}'"; } $sqlPk = implode(", ", $sqlPk);
+    $baseRefTableName = $this->getRootTableName() . "__#REF__";
+    $refRows = array();// $fieldName => [ pk, ... ]
+    foreach ($refs as $fieldName => $type) {
+      $acl = $this->getAclFromTypeName($type);
+      $refTableName = $this->getRootTableName() . "__#REF__" . $acl->getRootTableName();
+      $refRows[$fieldName] = DB::getPDO()->fetchAllByKey("
+        select r.*
+        from `{$refTableName}` r
+        where r.PRIMARY_KEY in({$sqlPk})
+        -- TODO and r.INSTANCE = '{$refInstanceName}' -- for multiple types based on the same root table
+      ", $key = 'PRIMARY_KEY');
+      if($DBG>2){echo 'C.'.get_class($this).' L.' . __LINE__ . " getItems loop(\$fieldName:{$fieldName}) \$refTableName:({$refTableName}), \$refRows[$fieldName]";print_r($refRows[$fieldName]);echo"\n";}
+    }
+    foreach ($refRows as $fieldName => $refList) {
+      foreach ($refList as $pk => $ref) {
+        $items[ $pk ][ $fieldName ][] = array('xlink' => "{$refs[$fieldName]}.{$ref['REMOTE_PRIMARY_KEY']}");
+      }
+    }
+  }
+  public function addItem($itemTodo) {
+    return $this->parentAcl->addItem($itemTodo);
+  }
+  public function updateItem($itemPatch) {
+    $itemPatch['ID'] = $itemPatch['id'];
+    unset($itemPatch['id']);
+    return $this->parentAcl->updateItem($itemPatch);
+  }
+
+  public function getGeomFieldType($fieldName) { return null; }
+  public function getPrimaryKeyField() { return 'id'; }
+  public function getID() { return 0; }
+  public function getAttributesFromZasoby() {
+		$attributes = array();// fldName => [ 'id_zasob' => int, 'label' => str, 'description' => str ]
+    // if ($acl->hasFieldPerm($idZasob, 'W')) $elNode->setAttributeNS($rootWfsNsUri, "{$rootWfsNs}:allow_write", "true");
+    // if ($acl->hasFieldPerm($idZasob, 'C')) $elNode->setAttributeNS($rootWfsNsUri, "{$rootWfsNs}:allow_create", "true");
+    // if (!$acl->hasFieldPerm($idZasob, 'R')) $elNode->setAttributeNS($rootWfsNsUri, "{$rootWfsNs}:allow_read", "false");
+		return $attributes;
+	}
+  public function isEnumerationField($fieldName) { return false; }
+  public function validateFieldAction($fieldName, $taskPerm, $record = null) {
+    if ('File' == $fieldName) {
+      // return 'ref:p5_objects:File';
+      return true;
+    }
+    if ('id' == $fieldName) $fieldName = 'ID';
+		return $this->parentAcl->isAllowed($fieldID = $this->parentAcl->getFieldIdByName($fieldName), $taskPerm, $record);
+	}
+  public function getXsdFieldType($fieldName) {
+    if ('File' == $fieldName) return 'ref:p5_objects:File';
+    return $this->parentAcl->getXsdFieldType($fieldName);
+  }
+  public function isGeomField($fldName) { return $this->parentAcl->isGeomField($fldName); }
+
+}

+ 4 - 0
SE/se-lib/UserAcl.php

@@ -165,6 +165,10 @@ class UserAcl {
 		} else if ('objects' == $sourceName && 'File' == $objName) {
 			Lib::loadClass('FileStorageAcl');
 			return new FileStorageAcl();
+		} else if ('objects' == $sourceName && 'TestPerms' == $objName) {
+			// TODO: ObjectAcl::getAcl($objName);
+			Lib::loadClass('TestPermsStorageAcl');
+			return new TestPermsStorageAcl();
 		} else throw new HttpException("Not Implemented", 501);
 		return false;
 	}