فهرست منبع

Merge branch 'master' of ssh://biuro.biall-net.pl:2222/plabudda/se

Mariusz Muszyński 8 سال پیش
والد
کامیت
9badfe2410

+ 50 - 23
SE/se-lib/AclQueryFeatures.php

@@ -73,6 +73,7 @@ class AclQueryFeatures {
       default: {
         switch ($fieldType) {
           case 'xsd:number':
+          case 'xsd:int':
           case 'xsd:integer': {
             if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery];
             return ['=', $searchQuery];
@@ -458,13 +459,13 @@ class AclQueryFeatures {
       $acl = $this->_acl;
       $cols = array_filter($cols, function ($fieldQuery) use ($acl) {
         list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
-        if (empty($subFieldQuery)) return false;
+        // if (empty($subFieldQuery)) return false;
         return !$acl->isLocalField($fieldName);
       });
       foreach ($cols as $fieldQuery) { // group by fieldName
         list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
         if (!array_key_exists($fieldName, $this->_selectRemote)) $this->_selectRemote[$fieldName] = [];
-        $this->_selectRemote[$fieldName][] = $subFieldQuery;
+        if (!empty($subFieldQuery)) $this->_selectRemote[$fieldName][] = $subFieldQuery;
       }
     }
     return $this->_selectRemote;
@@ -481,27 +482,53 @@ class AclQueryFeatures {
     return $rows;
   }
 
-  public function fetchRowRefs($row) {
-    if (!$row) return $row;
-    $sqlPk = $this->getAclSqlPrimaryKeyField();
-    $primaryKey = $row[$sqlPk];
-    DBG::log($row, 'array', "DBG primaryKey '{$primaryKey}'");
-    if (!$primaryKey) throw new Exception("Missing primaryKey");
-    foreach ($this->getSelectRemote() as $fieldName => $cols) {
-      DBG::log($cols, 'array', "add select remote '{$fieldName}' \$cols");
-      $items = ACL::getAclByTypeName($fieldName)->buildQuery([
-        'cols' => $cols,
-        '__backRef' => [
-          'namespace' => $this->_acl->getNamespace(),
-          'primaryKey' => $primaryKey,
-          'fieldName' => $fieldName,
-        ]
-      ])->getItems();
-      DBG::log($items, 'array', "TODO: add remote items '{$fieldName}' \$items");
-      $row[$fieldName] = $items;
-    }
-    return $row;
-  }
+	public function fetchRowRefs($row) {
+		if (!$row) return $row;
+		$sqlPk = $this->getAclSqlPrimaryKeyField();
+		$primaryKey = $row[$sqlPk];
+		DBG::log($row, 'array', "DBG primaryKey '{$primaryKey}'");
+		if (!$primaryKey) throw new Exception("Missing primaryKey");
+		foreach ($this->getSelectRemote() as $fieldName => $cols) {
+			DBG::log($cols, 'array', "add select remote '{$fieldName}' \$cols");
+
+			$xsdType = $this->_acl->getXsdFieldType($fieldName);
+			if ('ref:' === substr($xsdType, 0, 4) && empty($cols)) { // only xlink's
+				DBG::log("TODO: add remote xlink's '{$fieldName}' \$items[{$primaryKey}]");
+				$refTable = ACL::getRefTable($this->_acl->getNamespace(), $fieldName);
+				DBG::log($refTable, 'string', "TODO: add remote xlink's '{$fieldName}' \$items[{$primaryKey}] from refTable");
+				if ($refTable) {
+					$xlinks = DB::getPDO()->fetchAll("
+						select r.REMOTE_PRIMARY_KEY
+						from `{$refTable}` r
+						where r.PRIMARY_KEY = '{$primaryKey}'
+					");
+					DBG::log($xlinks, 'array', "TODO: add remote xlink's '{$fieldName}' \$items[{$primaryKey}]");
+					$row[$fieldName] = array_map(function ($refInfo) use ($fieldName) {
+						$ns = Core_AclHelper::parseTypeName($fieldName);
+						return [
+							// '_ns' => $ns,
+							'xlink' => "{$ns['url']}#{$ns['name']}.{$refInfo['REMOTE_PRIMARY_KEY']}",
+						];
+					}, $xlinks);
+					DBG::log($row[$fieldName], 'array', "TODO: remote xlinks: \$items[{$primaryKey}][{$fieldName}]");
+				}
+			} else if ('ref:' === substr($xsdType, 0, 4)) { // cols
+				$items = ACL::getAclByTypeName($fieldName)->buildQuery([
+					'cols' => $cols,
+					'__backRef' => [
+						'namespace' => $this->_acl->getNamespace(),
+						'primaryKey' => $primaryKey,
+						'fieldName' => $fieldName,
+					]
+				])->getItems();
+				DBG::log($items, 'array', "TODO: add remote items '{$fieldName}' \$items");
+				$row[$fieldName] = $items;
+			} else { // TODO: field is not ref
+				DBG::log($items, 'array', "NotImplemented - add remote items for non ref field \$items[{$primaryKey}][{$fieldName}]?");
+			}
+		}
+		return $row;
+	}
 
   public function getAclSqlPrimaryKeyField() {
     return ($this->_acl instanceof Core_AclBase)

+ 3 - 11
SE/se-lib/AntAclBase.php

@@ -9,7 +9,7 @@ Lib::loadClass('Core_AclBase');
 class AntAclBase extends Core_AclBase {
 
   public function __construct($zasobID = 0) {
-    $this->_zasobID = $zasobID;
+    $this->_zasobID = (int)$zasobID;
     $this->_name = '';
     $this->_namespace = '';
     $this->_rootTableName = '';
@@ -18,6 +18,7 @@ class AntAclBase extends Core_AclBase {
     $this->_primaryKey = '';
     $this->_fields = [];
   }
+  public function getDB() { return $this->_db; }
   public function getName() { return $this->_name; }
   public function getNamespace() { return $this->_namespace; }
   public function getRootNamespace() { return $this->_rootNamespace; }
@@ -189,16 +190,7 @@ class AntAclBase extends Core_AclBase {
       return $_cache[$idZasob];
     }
 
-    if (empty($conf)) {
-      throw new Exception("Brak danych konfiguracyjnych do obiektu ant nr {$idZasob}");
-
-      // TODO: fetch conf by $idZasob (or find $namespace first)
-      // Lib::loadClass('SchemaFactory');
-      // $objectStorage = SchemaFactory::loadDefaultObject('SystemObject');
-      // $item = $objectStorage->getItem($namespace, [
-      // 	'propertyName' => '*,field'
-      // ]);
-    }
+    if (empty($conf)) throw new Exception("Brak danych konfiguracyjnych do obiektu ant nr {$idZasob}");
     DBG::log($conf, 'array', 'AntAclBase::buildInstance $conf');
     $acl = new AntAclBase($idZasob);
     $acl->_name = $conf['name'];

+ 110 - 21
SE/se-lib/Api/WfsDataServer.php

@@ -4,6 +4,7 @@ Lib::loadClass('Api_WfsServerBase');
 Lib::loadClass('Api_WfsException');
 Lib::loadClass('Api_WfsGeomTypeConverter');
 Lib::loadClass('Api_WfsNs');
+Lib::loadClass('Core_XmlWriter');
 Lib::loadClass('DBG');
 
 class Api_WfsDataServer extends Api_WfsServerBase {
@@ -172,7 +173,7 @@ class Api_WfsDataServer extends Api_WfsServerBase {
 		$rootWfsNs = 'p5';
 		$rootWfsNsUri = "{$baseNsUri}";
 		$wfsNs = $args['typePrefix'];
-		$wfsNsUri = "{$baseNsUri}/" . substr($args['typePrefix'], 3);
+		$wfsNsUri = "{$baseNsUri}/" . ('p5_' === substr($args['typePrefix'], 0, 3) ? substr($args['typePrefix'], 3) : $args['typePrefix']);
 		$featureTypeUri = $this->getBaseUri() . "?SERVICE=WFS&VERSION=1.0.0&TYPENAME={$args['xsd:type']}&REQUEST=DescribeFeatureType";
 
 		// get BBox from geom_field (only one geom fld is allowed)
@@ -199,14 +200,74 @@ class Api_WfsDataServer extends Api_WfsServerBase {
 		if (!empty($args['filterFields'])) $searchParams['cols'] = $args['filterFields'];// PropertyName
 		if (!empty($args['primaryKey'])) $searchParams['primaryKey'] = $args['primaryKey'];// featureID
 		if (!empty($args['bbox'])) $searchParams['f_the_geom'] = "BBOX:{$args['bbox']}";
+
+		$contextFieldList = []; // convert $args['filterFields'] to field list
+		$schemaCache = array();
+		try {
+			$acl__getAllFieldNames = function ($listFields) {
+				return array_map(function ($field) {
+					return $field['fieldNamespace'];
+				}, $listFields);
+			};
+			$acl__getLocalFieldNames = function ($listFields) {
+				return array_map(function ($field) {
+					return $field['fieldNamespace'];
+				}, array_filter($listFields, function ($field) {
+					return $field['isLocal'];
+				}));
+			};
+			DBG::log($acl->getFields(), 'array', "\$contextFieldList ACL fields");
+			if (empty($args['filterFields'])) { // get all local fields
+				// $contextFieldList = $acl__getLocalFieldNames($acl->getFields());
+				$contextFieldList = $acl__getAllFieldNames($acl->getFields());
+			} else {
+				foreach ($args['filterFields'] as $fieldXPath) {
+					if ('*' === $fieldXPath) {
+						$contextFieldList = array_merge($contextFieldList, $acl__getLocalFieldNames($acl->getFields()));
+					} else if (false === strpos($fieldXPath, '/') && false === strpos($fieldXPath, ':')) {
+						$contextFieldList[] = $fieldXPath;
+					} else if (false === strpos($fieldXPath, '/') && false !== strpos($fieldXPath, ':')) {
+						$contextFieldList[] = $fieldXPath;
+						$fieldNs = str_replace(['__x3A__', ':'], '/', $fieldXPath);
+						$schemaCache[$fieldNs] = SchemaFactory::loadDefaultObject('SystemObject')->getItem($fieldNs, [ 'propertyName' => '*,field' ]);
+						DBG::log($schemaCache[$fieldNs], 'array', "\$schemaCache[{$fieldNs}]");
+					} else if ('/*' === substr($fieldXPath, -2) && false === strpos(substr($fieldXPath, 0, -2), '/')) {
+						$fieldName = substr($fieldXPath, 0, -2);
+						$contextFieldList[] = $fieldName;
+						$xsdType = $acl->getXsdFieldType($fieldName);
+						if ('ref:' !== substr($xsdType, 0, 4)) throw new Exception("Error Processing Request - field '{$fieldXPath}' type is not ref '/*' is not allowed");
+						$fieldNs = str_replace(['__x3A__', ':'], '/', substr($xsdType, 4));
+						if (!array_key_exists($fieldNs, $schemaCache)) {
+							$schemaCache[$fieldNs] = SchemaFactory::loadDefaultObject('SystemObject')->getItem($fieldNs, [ 'propertyName' => '*,field' ]);
+							DBG::log($schemaCache[$fieldNs], 'array', "\$schemaCache[{$fieldNs}]");
+						}
+						$fieldPrefix = "{$fieldName}";
+						$contextFieldList = array_merge($contextFieldList, array_map(function ($fieldName) use ($fieldPrefix) {
+							return "{$fieldPrefix}/{$fieldName}";
+						}, $acl__getLocalFieldNames($schemaCache[$fieldNs]['field'])));
+					} else {
+						$fieldName = trim($fieldXPath, '*/');
+						DBG::log(['$fieldXPath'=>$fieldXPath, '$fieldName'=>$fieldName], 'array', "\$contextFieldList TODO");
+					}
+				}
+			}
+			DBG::log($contextFieldList, 'array', "\$contextFieldList");
+
+			$searchParams['cols'] = $contextFieldList;
+		} catch (Exception $e) {
+			DBG::log($e);
+			throw $e;
+		}
+
 		DBG::log([ 'msg'=>'getItems - $searchParams', '$searchParams'=>$searchParams ]);
 		if($DBG){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">get_total (F.' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($jsonData->total);echo'</pre>';}
 		$queryFeatures = $acl->buildQuery($searchParams);
 		$items = $queryFeatures->getItems();
 		DBG::log([ 'msg'=>'getItems - $items', '$items'=>$items ]);
 
+
 		header('Content-type: application/xml; charset=utf-8');
-		$xmlWriter = new XMLWriter();
+		$xmlWriter = new Core_XmlWriter();
 		$xmlWriter->openUri('php://output');
 		// $xmlWriter->openMemory();// DBG
 		$xmlWriter->setIndent(true);
@@ -220,9 +281,12 @@ class Api_WfsDataServer extends Api_WfsServerBase {
 		$xmlWriter->writeAttribute('xmlns:gml', 'http://www.opengis.net/gml');
 		$xmlWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
 		$xmlWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
-		$xmlWriter->writeAttribute("xmlns:{$wfsNs}", $wfsNsUri);
+		$xmlWriter->writeAttribute("xmlns:{$wfsNs}", $wfsNsUri); // TODO: BUG $wfsNsUri
 		if (!$simple) $xmlWriter->writeAttribute("xmlns:{$rootWfsNs}", $rootWfsNsUri);
-		$xmlWriter->writeAttribute('xsi:schemaLocation', "{$wfsNsUri} {$featureTypeUri}");
+		foreach ($schemaCache as $childSchema) {
+			$xmlWriter->writeAttribute("xmlns:{$childSchema['nsPrefix']}", "{$rootWfsNsUri}/" . str_replace('__x3A__', '/', $childSchema['nsPrefix']));
+		}
+		$xmlWriter->writeAttribute('xsi:schemaLocation', "{$wfsNsUri} {$featureTypeUri}"); // TODO: BUG $wfsNsUri
 		$xmlWriter->writeAttribute('numberMatched', 'unknown'); // TODO: return total items if simple query (without prefix, small total number, maxFeatures set, etc.)
 		// NOTE: for client: if numberMatched == 'unknown' then request with resultType = 'hits'
 		$xmlWriter->writeAttribute('numberReturned', count($items));
@@ -235,7 +299,6 @@ class Api_WfsDataServer extends Api_WfsServerBase {
 
 			if (!empty($geomFld)) DBG::log(['msg'=>"item[{$itemKey}] ({$geomFld})isEmpty(".empty($item[$geomFld])."):", '$item['.$geomFld.']'=>$item[$geomFld]]);
 			DBG::log([ 'msg'=>">>> loop({$itemKey})", '$item'=>$item ]);
-
 			$xmlWriter->startElement('gml:featureMember');
 				$xmlWriter->startElement("{$wfsNs}:{$type}");
 					$xmlWriter->writeAttribute('fid', "{$type}.{$itemKey}");
@@ -257,23 +320,49 @@ class Api_WfsDataServer extends Api_WfsServerBase {
 							$xmlWriter->endElement();// {$wfsNs}:{$fldName}
 						} else if (is_array($item[$fldName])) {// TODO: by struct - REF field
 							DBG::log([ 'msg'=>">>> loop({$itemKey}) REF item[{$itemKey}][{$fldName}]", '$item'=>$item[$fldName] ]);
-							if (1 == count($item[$fldName])) {
-								$xlink = $item[$fldName][0]['xlink'];
-								$xlinkParts = explode(':', $xlink);
-								if (2 != count($xlinkParts)) throw new Exception("Error Processing Request - wrong xlink format for ".$acl->getName().".{$itemKey}/{$fldName}");
-								$xlinkParts[0] = Api_WfsNs::getNsUri($xlinkParts[0]);
-								$xlink = implode('#', $xlinkParts);
-								$xmlWriter->startElement("{$wfsNs}:{$fldName}");
-									if (!$simple && !$acl->canReadObjectField($fldName, (object)$item)) {
-										$xmlWriter->writeAttribute("{$rootWfsNs}:allow_read", "false");
-									}
-									if (!$simple && $acl->canWriteObjectField($fldName, (object)$item)) {
-										$xmlWriter->writeAttribute("{$rootWfsNs}:allow_write", "true");
-									}
-									$xmlWriter->writeAttribute('xlink:href', $xlink);
-								$xmlWriter->endElement();// {$wfsNs}:{$fldName}
+							if (empty($item[$fldName])) {
+								$xmlWriter->h($fldName);
+							// } else if (1 == count($item[$fldName]) && !empty($item[$fldName][0]['xlink'])) {
+							// 	$xlink = $item[$fldName][0]['xlink'];
+							// 	$xlinkParts = explode(':', $xlink);
+							// 	if (2 != count($xlinkParts)) throw new Exception("Error Processing Request - wrong xlink format for ".$acl->getName().".{$itemKey}/{$fldName}");
+							// 	$xlinkParts[0] = Api_WfsNs::getNsUri($xlinkParts[0]);
+							// 	$xlink = implode('#', $xlinkParts);
+							// 	$xmlWriter->startElement("{$wfsNs}:{$fldName}");
+							// 		if (!$simple && !$acl->canReadObjectField($fldName, (object)$item)) {
+							// 			$xmlWriter->writeAttribute("{$rootWfsNs}:allow_read", "false");
+							// 		}
+							// 		if (!$simple && $acl->canWriteObjectField($fldName, (object)$item)) {
+							// 			$xmlWriter->writeAttribute("{$rootWfsNs}:allow_write", "true");
+							// 		}
+							// 		$xmlWriter->writeAttribute('xlink:href', $xlink);
+							// 	$xmlWriter->endElement();// {$wfsNs}:{$fldName}
 							} else {
-								throw new Exception("Error Processing Request - too many refs for ".$acl->getName().".{$itemKey}/{$fldName}");
+								// $xmlWriter->writeComment("TODO: ".$acl->getName().".{$itemKey}/{$fldName} ...");
+								$fieldNs = str_replace(['__x3A__', ':'], '/', $fldName); // substr($xsdType, 4));
+								if (!array_key_exists($fieldNs, $schemaCache)) {
+									$xmlWriter->writeComment("Error Processing Request - field is not ref ".$acl->getName().".{$itemKey}/{$fldName}");
+								} else {
+									foreach ($item[$fldName] as $refItem) {
+										DBG::log($refItem, 'array', "\$refItem fld({$fldName})");
+										if (1 == count($refItem) && !empty($refItem['xlink'])) {
+											$xmlWriter->startElement($schemaCache[$fieldNs]['typeName']);
+											$xmlWriter->writeAttribute("xlink:href", $refItem['xlink']);
+											$xmlWriter->endElement();
+										} else {
+											$xmlWriter->startElement($schemaCache[$fieldNs]['typeName']);
+											foreach ($schemaCache[$fieldNs]['field'] as $field) {
+												// $xmlWriter->writeComment("REF field ({$field['fieldNamespace']}) value({$refItem[$field['fieldNamespace']]})");
+												if (array_key_exists($field['fieldNamespace'], $refItem)) {
+													$xmlWriter->startElement("{$schemaCache[$fieldNs]['nsPrefix']}:{$field['fieldNamespace']}");
+													$xmlWriter->text($refItem[$field['fieldNamespace']]);
+													$xmlWriter->endElement();
+												}
+											}
+											$xmlWriter->endElement();
+										}
+									}
+								}
 							}
 						} else if ('xsd:base64Binary' === $acl->getXsdFieldType($fldName)) {
 							if (empty($item[$fldName]) && '0' !== $item[$fldName]) continue;

+ 121 - 53
SE/se-lib/Api/WfsServerBase.php

@@ -5,7 +5,7 @@ Lib::loadClass('Api_WfsGeomTypeConverter');
 Lib::loadClass('Api_WfsNs');
 Lib::loadClass('Request');
 Lib::loadClass('Core_AclHelper');
-Lib::loadClass('Core_XMLWriter');
+Lib::loadClass('Core_XmlWriter');
 
 class Api_WfsServerBase {
 
@@ -52,6 +52,29 @@ class Api_WfsServerBase {
 			}
 		}
 
+		try { // TODO: use object cache `CRM_#CACHE_ACL_OBJECT`
+			$namespace = str_replace([':', '__x3A__'], '/', $typeName);
+			Lib::loadClass('SchemaFactory');
+			$objItem = SchemaFactory::loadDefaultObject('SystemObject')->getItem($namespace, ['propertyName'=>"*,field"]);
+			DBG::log($objItem, 'array', "DBG objItem({$namespace})");
+			if (!$objItem['idZasob']) throw new Exception("Missing idZasob for namespace '{$namespace}'");
+			if (!in_array($objItem['_type'], [
+				// 'TableAcl', // TODO: TEST - to replace TableAcl by AntAcl or use object with namespace + '/tableName'?
+				'AntAcl',
+			])) throw new Exception("Not Implemented acl type '{$objItem['_type']}'");
+			if (!$objItem['isObjectActive']) {
+				if (!$objItem['hasStruct']) throw new Exception("namespace has no structure '{$namespace}'");
+				if (!$objItem['isStructInstalled']) throw new Exception("namespace structure not installed '{$namespace}'");
+				throw new Exception("namespace is not activated '{$namespace}'");
+			}
+
+			Lib::loadClass('AntAclBase');
+			$acl = AntAclBase::buildInstance($objItem['idZasob'], $objItem);
+			return $acl;
+		} catch (Exception $e) {
+		  DBG::log($e);
+		}
+
 		$typeEx = explode(':', $typeName);
 		$sourceName = $typeEx[0];
 		$objName = $typeEx[1];
@@ -107,8 +130,14 @@ class Api_WfsServerBase {
 		if (empty($args['ogc:filter'])) {// read ogc:Filter from POST body if not defined in GET
 			$reqBody = Request::getRequestBody();
 			if (!empty($reqBody)) {
-				$args['ogc:filter'] = $this->convertOgcFilterFromRequestBody($reqBody);
-				if (empty($args['ogc:filter'])) throw new Api_WfsException("Error Processing ogc:Filter", 501);
+				$parsedRequest = $this->parseOgcFilterRequest($reqBody);
+				DBG::log($parsedRequest, 'array', 'parsed ogc query request');
+				if (!empty($parsedRequest['ogc:Filter'])) $args['ogc:filter'] = $parsedRequest['ogc:Filter'];
+				if (!empty($parsedRequest['wfs:PropertyName'])) {
+					foreach ($parsedRequest['wfs:PropertyName'] as $fieldXpath) {
+						$args['filterFields'][] = $fieldXpath;
+					}
+				}
 			}
 		}
 
@@ -116,7 +145,6 @@ class Api_WfsServerBase {
 
 		$args['wfs:propertyName'] = trim(V::get('propertyname', '', $lowerArgs));// TODO: fields to show - split by ','
 		if (strlen($args['wfs:propertyName']) > 0) {
-			$args['filterFields'] = array();
 			$exFields = explode(',', $args['wfs:propertyName']);
 			foreach ($exFields as $fieldName) {
 				$fieldName = trim($fieldName);
@@ -169,11 +197,11 @@ class Api_WfsServerBase {
 	public function _getCapabilities($wfsServerUrl, $serviceTitle, $serviceDescription) {
 		if (V::get('DBG_ACL', '', $_GET)) {
 			{
-				echo "Core_AclHelper::getAclList = [" . "\n";
-				foreach (Core_AclHelper::getAclList() as $typeName) {
+				echo "Core_AclHelper::getCustomAclList = [" . "\n";
+				foreach (Core_AclHelper::getCustomAclList() as $typeName) {
 					echo "|\t{$typeName}" . "\n";
 				}
-				echo "]// .EOF Core_AclHelper::getAclList" . "\n";
+				echo "]// .EOF Core_AclHelper::getCustomAclList" . "\n";
 			}
 
 			$fullTblAclList = $this->_usrAcl->getTablesAcl();
@@ -181,7 +209,7 @@ class Api_WfsServerBase {
 			die("\n" . '.EOF - DBG_ACL');
 		}
 		header('Content-type: application/xml; charset=utf-8');
-		$xmlWriter = new Core_XMLWriter();
+		$xmlWriter = new Core_XmlWriter();
 		if (!$xmlWriter) throw new HttpException("Error no XMLWriter", 404);
 		$xmlWriter->openUri('php://output');
 		$xmlWriter->setIndent(true);
@@ -299,10 +327,12 @@ class Api_WfsServerBase {
 
 		$featureTypeNodes = [];
 		foreach ($this->_getTableAclList() as $tblAcl) {
+			$ns = Core_AclHelper::parseNamespaceUrl($tblAcl->getNamespace());
+			DBG::log(['ns'=>$ns, 'acl_ns'=>$tblAcl->getNamespace(), 'cls'=>get_class($tblAcl)], 'array', "tblAcl [".$tblAcl->getID()."]");
 			$dataSourceName = 'default_db';// TODO: $tblAcl->getSourceName()
 			$prefix = "p5_{$dataSourceName}";
 			$featureTypeNodes[] = [ 'FeatureType', [ "xmlns:{$prefix}" => Api_WfsNs::getNsUri($prefix) ], [
-				[ 'Name', $prefix . ':' . $tblAcl->getName() ],
+				[ 'Name', "p5_" . "{$ns['prefix']}:{$ns['name']}" ], // TODO: remove 'p5_' prefix
 				[ 'Title', $tblAcl->getRawLabel() ],
 				[ 'Abstract', $tblAcl->getRawOpis() ],
 				[ 'Keywords', implode(', ', [ $tblAcl->getID(), $tblAcl->getName(), $tblAcl->getRawLabel() ]) ],
@@ -312,20 +342,25 @@ class Api_WfsServerBase {
 					'miny' => "38.8575126897477",
 					'maxx' => "9.838674658246807",
 					'maxy' => "41.31378404137082"], null ]
-			]];
+			] ];
 		}
-		foreach (Core_AclHelper::getAclList() as $typeName) {
+		foreach (Core_AclHelper::getCustomAclList() as $typeName) {
 			list($prefix, $name) = explode(':', $typeName);
 			$featureTypeNodes[] = [ 'FeatureType', [ "xmlns:{$prefix}" => Api_WfsNs::getNsUri($prefix) ], [
-				[ 'Name', $name ],
+				[ 'Name', "{$prefix}:{$name}" ],
 				[ 'Title', $name ],
 				[ 'Abstract', $name ],
 				[ 'Keywords', $name ],
 				[ 'SRS', 'EPSG:4326' ],
-			]];
+			] ];
 		}
 		$xmlWriter->startElement('FeatureTypeList');
 		$xmlWriter->h('Operations', ['Query', 'Insert', 'Update', 'Delete', 'Lock']);
+		if (DBG::isActive()) {
+			DBG::log(array_map(function ($feature) {
+				return "{$feature[2][0][1]}, ".array_values($feature[1])[0];
+			}, $featureTypeNodes), 'array', "\$featureTypeNodes");
+		}
 		foreach ($featureTypeNodes as $node) {
 			$xmlWriter->h($node);
 		}
@@ -1263,7 +1298,7 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 		$featureTypeUri = $this->getBaseUri() . "?SERVICE=WFS&VERSION=1.0.0&TYPENAME={$args['xsd:type']}&REQUEST=DescribeFeatureType";
 
 		header('Content-type: application/xml; charset=utf-8');
-		$xmlWriter = new Core_XMLWriter();
+		$xmlWriter = new Core_XmlWriter();
 		if (!$xmlWriter) throw new HttpException("Error no XMLWriter", 404);
 		$xmlWriter->openUri('php://output');
 		$xmlWriter->setIndent(true);
@@ -1290,7 +1325,7 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 		$acl = $this->getAclFromTypeName("{$nsPrefix}:{$name}");
 
 		header('Content-type: application/xml; charset=utf-8');
-		$xmlWriter = new Core_XMLWriter();
+		$xmlWriter = new Core_XmlWriter();
 		if (!$xmlWriter) throw new HttpException("Error no XMLWriter", 404);
 		$xmlWriter->openUri('php://output');
 		$xmlWriter->setIndent(true);
@@ -1414,7 +1449,7 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 			if (!array_key_exists($aclNamespaceUri, $nsMap)) $nsMap[$aclNamespaceUri] = $acl->getSourceName();
 		}
 
-		$xmlWriter = new Core_XMLWriter();
+		$xmlWriter = new Core_XmlWriter();
 		if (!$xmlWriter) throw new HttpException("Error no XMLWriter", 404);
 		$xmlWriter->openMemory();// openUri('php://output');
 		$xmlWriter->setIndent(true);
@@ -1677,15 +1712,15 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 			if ($idDefaultDB != $tblAcl->getDB()) {// hide non default_db tables
 				continue;
 			}
-			try {
-				$acl = $this->getAclFromTypeName($typeName = "p5_{$dataSourceName}:{$tblName}");
-			} catch (Exception $e) {
-				// echo "Error for table({$tblName}): " . $e->getMessage() . "\n";
-			}
-			if (!$acl) {
-				// TODO: error log msg
-				continue;
-			}
+			// try {
+			// 	$acl = $this->getAclFromTypeName($typeName = "p5_{$dataSourceName}:{$tblName}");
+			// } catch (Exception $e) {
+			// 	// echo "Error for table({$tblName}): " . $e->getMessage() . "\n";
+			// }
+			// if (!$acl) {
+			// 	// TODO: error log msg
+			// 	continue;
+			// }
 			$tblAclList[] = $tblAcl;
 		}
 		return $tblAclList;
@@ -1705,38 +1740,71 @@ if($DBG){echo 'L.' . __LINE__ . ' $validateConvertedTransactionXsdString:';print
 		$this->_logger->DBG($reqLog, $lineNr, $funName, $className);
 	}
 
-	public function convertOgcFilterFromRequestBody($requestOgcFilter) {
-		$ogcFilter = '';
-		if (empty($requestOgcFilter)) return '';
-		{
-			$convertOgcFilterXslString .= <<<EOF
-<xsl:transform version="1.0"
-							 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
-							 xmlns:wfs="http://www.opengis.net/wfs"
-							 xmlns:ogc="http://www.opengis.net/ogc"
-							 xmlns:gml="http://www.opengis.net/gml">
-	<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
-	<xsl:template match="/">
-	<xsl:for-each select="//*[local-name() = 'GetFeature']">
-	  <xsl:copy-of select="ogc:Filter"/>
-	</xsl:for-each>
-	</xsl:template>
-</xsl:transform>
-EOF;
+	public function parseOgcFilterRequest($requestOgcFilter) {
+		// GetFeature: @maxFeatures, @traverseXlinkDepth, @traverseXlinkExpiry
+		// \-- (1..) wfs:Query: @typeName
+		//     \--- (0..) ---: wfs:PropertyName
+		//                   : wfs:XlinkPropertyName
+		//                   : ogc:Function
+		//          ogc:Filter
+		//          ogc:SortBy
+		//          \--- (1..) ogc:SortProperty
+		//               \---: ogc:PropertyName
+		//                   : ogc:SortOrder ( DESC / ASC )
+		// <ogc:SortBy>
+		//     <ogc:SortProperty>
+		//         <ogc:PropertyName>ASTA</ogc:PropertyName>
+		//         <ogc:SortOrder>ASC</ogc:SortOrder>
+		//     </ogc:SortProperty>
+		// </ogc:SortBy>
 
-			DBG::_('DBG_XML', '>2', "convertOgcFilterXslString", $convertOgcFilterXslString, __CLASS__, __FUNCTION__, __LINE__);
-			$convertTransactionXsl = new DOMDocument();
-			$convertTransactionXsl->loadXml($convertOgcFilterXslString);
+		if (empty($requestOgcFilter)) return '';
+		$requestXml = new DOMDocument();
+		$requestXml->loadXml($requestOgcFilter);
+		$nodesQuery = [];
+		foreach ($requestXml->getElementsByTagNameNS('http://www.opengis.net/wfs', 'Query') as $element) {
+			DBG::log($element->nodeName, 'array', "main loop - wfs:Query");
+			$nodesQuery[] = $element;
+		}
 
-			$requestXml = new DOMDocument();
-			$requestXml->loadXml($requestOgcFilter);
+		if (empty($nodesQuery)) { // legacy - try to find 'ogc:Filter'
+			$tagsFilter = [];
+			foreach ($requestXml->getElementsByTagNameNS('http://www.opengis.net/ogc', 'Filter') as $element) {
+				DBG::log($element->nodeName, 'array', "loop element (0 * wfs:Query)");
+				$tagsFilter[] = $requestXml->saveXML($element);
+			}
+			DBG::log($tagsFilter, 'array', "\$tagsFilter (0 * wfs:Query)");
+			return [
+				'ogc:Filter' => (!empty($tagsFilter))
+					? reset($tagsFilter) // TODO: only one ogc:Filter allowed
+					: ''
+			];
+		}
 
-			$proc = new XSLTProcessor();
-			$proc->importStylesheet($convertTransactionXsl);
-			$ogcFilter = $proc->transformToXML($requestXml);
-			DBG::_('DBG_XML', '>2', "ogcFilter", $ogcFilter, __CLASS__, __FUNCTION__, __LINE__);
+		if (1 === count($nodesQuery)) {
+			$tagsFilter = [];
+			foreach ($requestXml->getElementsByTagNameNS('http://www.opengis.net/ogc', 'Filter') as $element) {
+				// DBG::log($element->nodeName, 'array', "loop ogc:Filter (1 * wfs:Query)");
+				$tagsFilter[] = $requestXml->saveXML($element);
+			}
+			DBG::log($tagsFilter, 'array', "\$tagsFilter (1 * wfs:Query)");
+			$tagsWfsPropertyName = [];
+			foreach ($requestXml->getElementsByTagNameNS('http://www.opengis.net/wfs', 'PropertyName') as $element) {
+				// DBG::log($element->nodeValue, 'array', "loop wfs:PropertyName (1 * wfs:Query)");
+				$tagsWfsPropertyName[] = $element->nodeValue;
+			}
+			DBG::log($tagsWfsPropertyName, 'array', "\$tagsWfsPropertyName (1 * wfs:Query)");
+			return array_filter([
+				'ogc:Filter' => (!empty($tagsFilter))
+					? reset($tagsFilter)
+					: '',
+				'wfs:PropertyName' => (!empty($tagsWfsPropertyName))
+					? $tagsWfsPropertyName
+					: '',
+			], function ($value) { return !empty($value); });
 		}
-		return $ogcFilter;
+
+		throw new Exception("Not imeplemented multiple ogc:Query"); // multiple ogc:Query require ogc:Query/@typeName
 	}
 
 }

+ 29 - 0
SE/se-lib/Core/AclBase.php

@@ -867,4 +867,33 @@ if($DBG){die();}
     return '';
   }
 
+
+	public function getOpis() { return ''; } // $this->_opis; } // TODO: legacy
+	public function getRawOpis() { return ''; // TODO: legacy
+		// $opis = $this->_opis;
+		// if (!$opis) return $this->getRawLabel(100);
+		// $opis = strip_tags($opis);
+		// return $opis;
+	}
+	public function getLongRawLabel($posLimit = 30) { // TODO: legacy
+		return $this->getNamespace();
+		// $longLabel = $this->getRawLabel($posLimit);
+		// $opis = $this->_opis;
+		// if ($longLabel != $this->_name) {
+		// 	$longLabel .= ' ' . $this->_name;
+		// }
+		// return $longLabel;
+	}
+	public function getLongLabel($posLimit = 30) { // TODO: legacy
+    	return $this->getNamespace();
+		// $longLabel = $this->getRawLabel($posLimit);
+		// $opis = $this->_opis;
+		// if ($longLabel != $this->_name) {
+		// 	$longLabel .= ' <em>' . $this->_name . '</em>';
+		// }
+    	//
+		// $longLabel = '<span title="' . htmlspecialchars($opis) . '">' . $longLabel . '</span>';
+		// return $longLabel;
+	}
+
 }

+ 2 - 1
SE/se-lib/Core/AclHelper.php

@@ -227,7 +227,8 @@ class Core_AclHelper {// Helper class for Acl
 		return $rowFunList;
 	}
 
-  public static function getAclList() {// @usage Core_AclHelper::getAclList();// @returns array [ $typeName , ... ]
+  public static function getAclList() { return self::getCustomAclList(); } // TODO: RMME renamed to getCustomAclList
+  public static function getCustomAclList() {// @usage Core_AclHelper::getCustomAclList();// @returns array [ $typeName , ... ]
     $aclList = array();
     // Schema_AccessGroupStorageAcl, load by User::getAcl()->getObjectAcl('default_objects', $objName);
     //    $objClassName = "Schema_{$objName}StorageAcl";

+ 4 - 3
SE/se-lib/Lib.php

@@ -6,21 +6,22 @@ class Lib {
 
 
 	public static function loadClass($clsName) {
-		return self::_loadClass($clsName, true);
+		return self::_loadClass($clsName, $required = true);
 	}
 
 
 	public static function tryLoadClass($clsName) {
-		return self::_loadClass($clsName, false);
+		return self::_loadClass($clsName, $required = false);
 	}
 
 
 	/**
 	 * load Class.
-	 * 
+	 *
 	 * @example Core_Database_Mysql - check Core/Database/Mysql.php or Core_Database_Mysql.php file
 	 */
 	public static function _loadClass($clsName, $required = true) {
+		if (class_exists($clsName)) return true;
 		$path = APP_PATH_LIB . '/' . implode('/', explode('_', $clsName)) . '.php';
 		if (file_exists($path)) {
 			require_once $path;

+ 280 - 305
SE/se-lib/ProcesMenu.php

@@ -309,45 +309,45 @@ jQuery(document).ready(function() {
 		$MojeTestyTitle = "Ilość Procesów: {$proces_cnt}, Aktualnych testów: {$testy_ok},  Teoretycznych: {$testy_teoretyczne}, Praktycznych: {$testy_praktyczne}";
 
 		$userAcl = User::getAcl();
-		$tbls = $userAcl->getTablesAcl();
-		$urls = $userAcl->getUrls();
-
+		$tbls = [];
+		$urls = [];
+		$outUrls = array();
 		$outMenus = array();// typeName => label (order by label)
-		$rawOutMenus = array();
-		$labelsOutMenus = array();
-		$outBtnsMenus = array();
 		$typeNameToIdZasob = array();// $typeName => $idZasob
-		if (!empty($tbls)) {
-			foreach ($tbls as $kZasobID => $vTblAcl) {
-				$tblName = $vTblAcl->getName();
-				$typeName = "p5_default_db:{$tblName}";
-				$labelsOutMenus[$typeName] = $vTblAcl->getLongLabel();
-				$rawOutMenus[$typeName] = strtolower($vTblAcl->getLongRawLabel());
-				$typeNameToIdZasob[$typeName] = $kZasobID;
-				if ($userAcl->getPermsFiltrProcesId()) {
-					$outBtnsMenus[$typeName] = $vTblAcl->getRawLabel();
-				}
-			}
-		}
-		asort($rawOutMenus);
-		foreach ($rawOutMenus as $typeName => $rawLongLabel) $outMenus[$typeName] = $labelsOutMenus[$typeName];
 		if ($userAcl->getPermsFiltrProcesId()) {
-			asort($outBtnsMenus);
-		}
+			$tbls = $userAcl->getTablesAcl();
+			$urls = $userAcl->getUrls();
 
-		$outUrls = array();
-		if (!empty($urls)) {
-			/**
-			 * 	[147] => Array(
-       *       [TYPE] => URL
-       *       [DESC] => ?MENU_INIT=DODAJ_REKORDY_MIESZKAN_FUNC
-       *       [OPIS] => Narzedzie do wprowadzania zasobow mieszkan indywidualnych
-			 */
-			foreach ($urls as $kZasobID => $vTitle) {
-				$outUrls[$kZasobID] = $vTitle;
+			if (!empty($urls)) {
+				foreach ($urls as $kZasobID => $vTitle) {
+					$outUrls[$kZasobID] = $vTitle;
+				}
+			}
+			asort($outUrls);
+
+			$rawOutMenus = array();
+			$labelsOutMenus = array();
+			$outBtnsMenus = array();
+			// DBG::nicePrint($tbls, '$tbls');
+			if (!empty($tbls)) {
+				foreach ($tbls as $kZasobID => $vTblAcl) {
+					if (null == $vTblAcl) continue;
+					$tblName = $vTblAcl->getName();
+					$typeName = "p5_default_db:{$tblName}";
+					$labelsOutMenus[$typeName] = $vTblAcl->getLongLabel();
+					$rawOutMenus[$typeName] = strtolower($vTblAcl->getLongRawLabel());
+					$typeNameToIdZasob[$typeName] = $kZasobID;
+					// if ($userAcl->getPermsFiltrProcesId()) {
+						$outBtnsMenus[$typeName] = $vTblAcl->getRawLabel();
+					// }
+				}
+			}
+			asort($rawOutMenus);
+			foreach ($rawOutMenus as $typeName => $rawLongLabel) $outMenus[$typeName] = $labelsOutMenus[$typeName]; // TODO: mv to localStorage
+			if ($userAcl->getPermsFiltrProcesId()) {
+				asort($outBtnsMenus);
 			}
 		}
-		asort($outUrls);
 
 		$active = '';
 		$script_name = V::get('SCRIPT_NAME', '', $_SERVER);
@@ -393,19 +393,6 @@ jQuery(document).ready(function() {
 /*
  * $_SESSION['USER_PROFILE'][section][key] = val;
  */
-		$userBookmarks = UserBookmarks::getInstance();
-
-		$bookmarksJson = array();
-		$bookmarks = $userBookmarks->getBookmarks();
-		foreach ($bookmarks as $kZasobID => $vClass) {
-			if (array_key_exists($kZasobID, $tbls)) {
-				$bookmarksJson[] = (object)array('id'=>$kZasobID, 'name'=>$tbls[$kZasobID]->getName(), 'label'=>$tbls[$kZasobID]->getRawLabel(), 'opis'=>$tbls[$kZasobID]->getOpis(), 'type'=>'menu', 'class'=>$vClass);
-			}
-			else if (array_key_exists($kZasobID, $urls)) {
-				$bookmarksJson[] = (object)array('id'=>$kZasobID, 'name'=>$urls[$kZasobID], 'type'=>'url', 'class'=>$vClass);
-			}
-		}
-
 		$userGroupIdsCSV = User::getGroupsIds();
 		$userGroupIdsCSV = implode(',', $userGroupIdsCSV);
 
@@ -424,12 +411,36 @@ jQuery(document).ready(function() {
 		$lastProcesyFiltrIds = $treeProcesyFilter->get_arg('filtr_id');
 
 		$menuProcesViewedTblId = 0;
-		if ('VIEWTABLE_AJAX' == V::get('MENU_INIT', '', $_REQUEST)) {
-			$menuProcesViewedTblId = V::get('ZASOB_ID', 0, $_REQUEST, 'int');
+		if ('ViewTableAjax' == V::get('_route', '', $_REQUEST)) {
+			$namespace = V::get('namespace', '', $_REQUEST, 'word');
+			if (!$namespace) {
+				$typeName = V::get('typeName', '', $_REQUEST, 'word');
+				if ($typeName) {
+					$namespace = str_replace(['__x3A__', ':'], '/', $typeName);
+				}
+			}
+			if ($namespace) {
+				$zasobTableName = (
+					'default_db/' === substr($namespace, 0, strlen('default_db/'))
+					&& false === strpos(substr($namespace, strlen('default_db/')), '/')
+				)
+				? substr($namespace, strlen('default_db/'))
+				: $namespace;
+				if ($zasobTableName) {
+					$dbID = DB::getPDO()->getZasobId();
+					$menuProcesViewedTblId = DB::getPDO()->fetchValue("
+						select z.ID
+						from CRM_LISTA_ZASOBOW z
+						where z.`DESC` = '{$zasobTableName}'
+							and z.A_STATUS not in ('DELETED')
+							and z.`TYPE` = 'TABELA'
+							and z.PARENT_ID = '{$dbID}'
+					");
+				}
+			}
 		}
-		else if ('ViewTableAjax' == V::get('_route', '', $_REQUEST)) {
-			$typeName = V::get('typeName', '', $_REQUEST, 'word');
-			if (array_key_exists($typeName, $typeNameToIdZasob)) $menuProcesViewedTblId = $typeNameToIdZasob[$typeName];
+		else if ('VIEWTABLE_AJAX' == V::get('MENU_INIT', '', $_REQUEST)) {
+			$menuProcesViewedTblId = V::get('ZASOB_ID', 0, $_REQUEST, 'int');
 		}
 
 		?>
@@ -437,35 +448,67 @@ jQuery(document).ready(function() {
 	<div class="container-fluid">
 		<div class="collapse navbar-collapse">
 			<ul class="nav navbar-nav">
-				<li class="dropdown<?php if ($active == 'menu') echo ' active'; ?>">
-					<a href="#" class="dropdown-toggle" data-toggle="dropdown">Menu <b class="caret"></b></a>
-					<ul class="dropdown-menu" id="SE-menu-tables">
-						<?php foreach ($outMenus as $typeName => $vName) : $kZasobID = $typeNameToIdZasob[$typeName]; ?>
-							<li>
-								<a href="index.php?_route=ViewTableAjax&typeName=<?= $typeName; ?>">
-									<i class="bookmark-item-add-<?= $kZasobID; ?> bookmark-item-add glyphicon glyphicon-star-empty" title="Add to favorites" data-zasobid="<?= $kZasobID; ?>"></i>
-									<i class="bookmark-item-rem-<?= $kZasobID; ?> bookmark-item-rem glyphicon glyphicon-star" style="display:none" title="Remove from favorites" data-zasobid="<?= $kZasobID; ?>"></i>
-									<?php echo $vName; ?>
-								</a>
-							</li>
-						<?php endforeach; ?>
-					</ul>
-				</li>
-				<li class="dropdown">
-					<a href="#" class="dropdown-toggle" data-toggle="dropdown">Narzędzia <b class="caret"></b></a>
-					<ul class="dropdown-menu">
-						<?php foreach ($outUrls as $kZasobID => $vTitle) : ?>
-							<li>
-								<a href="index.php?FUNCTION_INIT=URL_INIT&ZASOB_ID=<?php echo $kZasobID; ?>" target="_blank" title="<?php echo $vTitle; ?>">
-									<i class="bookmark-item-add-<?php echo $kZasobID; ?> bookmark-item-add glyphicon glyphicon-star-empty" title="Add to favorites" data-zasobid="<?php echo $kZasobID; ?>"></i>
-									<i class="bookmark-item-rem-<?php echo $kZasobID; ?> bookmark-item-rem glyphicon glyphicon-star" style="display:none" title="Remove from favorites" data-zasobid="<?php echo $kZasobID; ?>"></i>
-									<code><?php echo $kZasobID; ?></code>
-									<?php echo (mb_strlen($vTitle, 'utf-8') > 100)? mb_substr($vTitle, 0, 100, 'utf-8') . '...' : $vTitle; ?>
-								</a>
-							</li>
-						<?php endforeach; ?>
-					</ul>
-				</li>
+				<?php
+					echo UI::h('li', [ 'class' => "dropdown" . ($active == 'menu' ? ' active' : '') ], [
+						UI::h('a',
+							($userAcl->getPermsFiltrProcesId())
+								? [
+										'href' => "#",
+										'class' => "dropdown-toggle",
+										'data-toggle' => "dropdown",
+									]
+								: [
+									'href' => "#",
+									'class' => "dropdown-toggle",
+									'onClick' => "return initP5MainMenuDropdown(this, 'SE-menu-tables');",
+								],
+							"Menu ". '<b class="caret"></b>'
+						),
+						UI::h('ul', [
+							'class' => "dropdown-menu",
+							'id' => "SE-menu-tables",
+						], array_map(function ($vName, $typeName) use ($typeNameToIdZasob) {
+							$kZasobID = $typeNameToIdZasob[$typeName];
+							return UI::h('li', [], [
+								UI::h('a', ['href'=>"index.php?_route=ViewTableAjax&typeName={$typeName}"], [
+									UI::h('i', ['class'=>"bookmark-item-add glyphicon glyphicon-star-empty", 'title'=>"Add to favorites", 'data-zasobid'=>$kZasobID]),
+									UI::h('i', ['class'=>"bookmark-item-rem glyphicon glyphicon-star", 'style'=>"display:none", 'title'=>"Remove from favorites", 'data-zasobid'=>$kZasobID]),
+									" " . $vName
+								])
+							]);
+						}, array_values($outMenus), array_keys($outMenus))),
+					]);
+
+					echo UI::h('li', [ 'class' => "dropdown"], [
+						UI::h('a',
+							($userAcl->getPermsFiltrProcesId())
+							? [
+									'href' => "#",
+									'class' => "dropdown-toggle",
+									'data-toggle' => "dropdown",
+								]
+							: [
+								'href' => "#",
+								'class' => "dropdown-toggle",
+								'onClick' => "return initP5UrlsMenuDropdown(this, 'SE-menu-urls');",
+							],
+							"Narzędzia " . '<b class="caret"></b>'
+						),
+						UI::h('ul', [
+							'class' => "dropdown-menu",
+							'id' => "SE-menu-urls"
+						], array_map(function ($label, $idZasob) {
+							return UI::h('li', [], [
+								UI::h('a', [ 'href' => "index.php?FUNCTION_INIT=URL_INIT&ZASOB_ID={$idZasob}", 'target' => "_blank", 'title' => $label ], [
+									UI::h('i', [ 'class' => "bookmark-item-add glyphicon glyphicon-star-empty", 'title' => "Add to favorites", 'data-zasobid' => $idZasob ]),
+									UI::h('i', [ 'class' => "bookmark-item-rem glyphicon glyphicon-star", 'style' => "display:none", 'title' => "Remove from favorites", 'data-zasobid' => $idZasob ]),
+									UI::h('code', [ 'style' => "margin-left:6px; margin-right:6px" ], (string)$idZasob),
+									(mb_strlen($label, 'utf-8') > 100)? mb_substr($label, 0, 100, 'utf-8') . '...' : $label,
+								]),
+							]);
+						}, array_values($outUrls), array_keys($outUrls))),
+					]);
+				?>
 				<li class="dropdown <?php if ($active == 'procesy') echo "active"; ?>">
 					<a id="ProcesMenuProcesDropdownLink" href="#" class="dropdown-toggle" data-toggle="dropdown">Procesy <b class="caret"></b></a>
 					<ul class="dropdown-menu">
@@ -872,242 +915,174 @@ jQuery(document).ready(function() {
 -->
 		</div><!-- /.navbar-collapse -->
 	</nav>
-	<?php if ($userAcl->getPermsFiltrProcesId()) : ?>
-		<div id="SE-menu-sub" style="clear:both;">
-			<a class="btn btn-xs btn-danger" href="index.php?FUNCTION_INIT=MENU_SELECT_PROCES&_action=setPermsAll" title="Wyłącz filtr uprawnień dla procesu <?php echo $userAcl->getPermsFiltrProcesId(); ?>">Wyłącz filtr uprawnień: <?php echo $userAcl->getPermsFiltrProcesId(); ?></a>
-			<?php foreach ($outBtnsMenus as $typeName => $label) : ?>
-				<a class="btn btn-xs btn-default" href="index.php?_route=ViewTableAjax&typeName=<?php echo $typeName; ?>" title="<?php echo $label; ?>"><?php echo V::strShortUtf8($label, 20); ?></a>
-			<?php endforeach; ?>
-			<?php foreach ($outUrls as $kZasobID => $vTitle) : ?>
-				<a class="btn btn-xs btn-default" href="index.php?FUNCTION_INIT=URL_INIT&ZASOB_ID=<?php echo $kZasobID; ?>" target="_blank" title="<?php echo $vTitle; ?>"><?php echo V::strShortUtf8($vTitle, 20); ?></a>
-			<?php endforeach; ?>
-		</div>
-	<?php else : ?>
-		<div id="SE-menu-sub" style="clear:both;"></div>
-<script>
-(function ($, undefined) {
-	var UserBookmarks = function() {
-		var priv = {}; //private api
-		var publ = {}; //public api
-
-		priv.options = {};
-		var defaults = {
-			url: '',  //webservice url
-			urlInit: true, // try to load services on init
-			preloadData: null,
-			menuid: '',
-			debug: false
-		};
-
-		var _cont; // container holding table
-		var _menu; // container holding stars
-		var _stateEdit = false;
-
-		/*
-		 initialize the plugin.
-		 */
-		priv.init = function() {
-			_cont = $(priv.options.id);
-			_menu = $(priv.options.menuid);
-
-			_menu.find('.bookmark-item-add').click(function(e){
-				e.preventDefault();
-				e.stopPropagation();
-				var zasobid = jQuery(this).data('zasobid');
-				priv.update('add_bookmark', zasobid);
-
-				var item = $('#bookmark-item-' + zasobid);
-				if (item) {
-					item.addClass('has_bookmark');
+	<?php
+
+		UI::inlineJS(APP_PATH_WWW . '/static/p5UI/menuStore.js');
+
+		$idFiltrProcesID = $userAcl->getPermsFiltrProcesId();
+		if ($idFiltrProcesID > 0) {
+			echo UI::h('div', [ 'id' => "SE-menu-sub", 'style' => "clear:both;" ], array_merge(
+				[
+					UI::h('a', [ 'class' => "btn btn-xs btn-danger", 'href' => "index.php?FUNCTION_INIT=MENU_SELECT_PROCES&_action=setPermsAll", 'title' => "Wyłącz filtr uprawnień dla procesu {$idFiltrProcesID}" ], "Wyłącz filtr uprawnień: {$idFiltrProcesID}"),
+				],
+				array_map(
+					function ($label, $typeName) {
+						return UI::h('a', [ 'class' => "btn btn-xs btn-default", 'href' => "index.php?_route=ViewTableAjax&typeName={$typeName}", 'title' => $label ], V::strShortUtf8($label, 20));
+					}, array_values($outBtnsMenus), array_keys($outBtnsMenus)
+				),
+				array_map(
+					function ($label, $idZasob) {
+						return UI::h('a', [ 'class' => "btn btn-xs btn-default", 'href' => "index.php?FUNCTION_INIT=URL_INIT&ZASOB_ID={$idZasob}", 'target' => "_blank", 'title' => $label ], V::strShortUtf8($label, 20));
+					}, array_values($outUrls), array_keys($outUrls)
+				)
+			));
+		} else {
+			echo UI::h('div', [ 'id' => "SE-menu-sub", 'style' => "clear:both" ]);
+			UI::inlineJS(APP_PATH_WWW . '/static/p5UI/userBookmarks.js');
+			// url: 'index-ajax.php?_cls=UserBookmarks',
+			echo UI::h('script', [], "
+				(function (global) {
+					jQuery('#SE-menu-sub').UserBookmarks({
+						url: 'index.php?_route=P5Menu',
+						store: global.p5UI__MenuStore,
+						debug: false
+					});
+				})(window);
+			");
+		}
+		echo '</div>';
+
+		echo UI::h('script', [], "
+			(function (global) {
+				if (!global.p5UI__MenuStore) throw 'Missing global.p5UI__MenuStore'
+
+				function p5BookmarksAdd(e, id) {
+					e.preventDefault()
+					e.stopPropagation()
+					global.p5UI__MenuStore.remoteUpdate({
+						'_postTask': 'addBookmark',
+						'_zasobID': id,
+					})
 				}
-			});
-			_menu.find('.bookmark-item-rem').click(function(e){
-				e.preventDefault();
-				e.stopPropagation();
-				var zasobid = jQuery(this).data('zasobid');
-				priv.update('remove_bookmark', zasobid);
-			});
-
-			if (priv.options.urlInit) priv.update();
-			if (priv.options.preloadData) priv.setData(priv.options.preloadData);
-
-			_cont.sortable();
-			_cont.on('sortupdate', priv.sort);
-
-		};
-
-		priv.setData = function(data) {
-			_cont.empty();
-			$.each(data, function(ind, item){
-				if ('type' in item) {
-					var l = $('<a></a>');
-					l.data('id', item.id);
-					l.addClass('btn');
-					l.addClass('btn-xs');
-					var label = item.name, title = '';
-					if (item.hasOwnProperty('class') && item['class'] != '') {
-						l.addClass(item.class);
-					} else {
-						l.addClass('btn-default');
-					}
-					if (item.type == 'menu') {
-						l.attr('href', 'index.php?_route=ViewTableAjax&typeName=' + 'p5_default_db:' + item.name);
-						if ('label' in item && item.label.length > 0) {
-							label = item.label;
-							title = item.label + ' (' + item.name + ')';
-						}
-						else if ('opis' in item && item.opis.length > 0) {
-							label = item.opis;
-							title = item.opis + ' (' + item.name + ')';
-						}
-					} else if (item.type == 'url') {
-						l.attr('href', 'index.php?FUNCTION_INIT=URL_INIT&ZASOB_ID=' + item.id);
-						l.attr('target', '_blank');
-					}
-					if (label.length > 20) {
-						var pos = label.indexOf(' - ');
-						if (pos > 20 || pos < 5) {
-							pos = 20;
-							l.text(label.substring(0, pos) + ' ...');
-						} else {
-							l.text(label.substring(0, pos));
-						}
-					} else {
-						l.text(label);
+				function p5BookmarksRemove(e, id) {
+					e.preventDefault()
+					e.stopPropagation()
+					global.p5UI__MenuStore.remoteUpdate({
+						'_postTask': 'removeBookmark',
+						'_zasobID': id,
+					})
+				}
+
+				global.p5BookmarksAdd = p5BookmarksAdd
+				global.p5BookmarksRemove = p5BookmarksRemove
+			})(window)
+		");
+
+		echo UI::h('script', [], "
+			(function (global) {
+				if (!global.p5UI__MenuStore) throw 'Missing global.p5UI__MenuStore'
+
+				function initP5MainMenuDropdown( btnNode, idSubMenu ) {
+					if (!btnNode._initialized) {
+						var jqDropdownTrigger = jQuery(btnNode)
+						var jqDropdownMenu = jQuery('#' + idSubMenu)
+						var jqDropdownParent = jqDropdownMenu.parent()
+
+						global.p5UI__MenuStore.subscribe(
+							(function (global, idSubMenu) {
+								return function renderP5MainMenuDropdown(data) {
+									var jqDropdownMenu = jQuery('#' + idSubMenu)
+									jqDropdownMenu.empty()
+									jqDropdownMenu.append(data.objects.map(function (item) {
+										var star = (-1 !== data.idsBookmarks.indexOf(item.id))
+											? '<i class=\"bookmark-item-rem glyphicon glyphicon-star\" title=\"Usuń z ulubionych\" onClick=\"return p5BookmarksRemove(event, ' + item.id + ')\"></i>'
+											: '<i class=\"bookmark-item-add glyphicon glyphicon-star-empty\" title=\"Dodaj do ulubionych\" onClick=\"return p5BookmarksAdd(event, ' + item.id + ')\"></i>'
+										return jQuery('<li>' +
+											'<a href=\"index.php?_route=ViewTableAjax&namespace=' + item.namespace + '\">' +
+												star +
+												' ' + item.label +
+											'</a>' +
+										'</li>');
+									}))
+								}
+							})(global, idSubMenu)
+						)
+
+						jqDropdownTrigger.attr('data-toggle', 'dropdown') // is required by bootstrap dorpdown.js evenf if is called via js
+
+						global.p5UI__MenuStore.forceUpdate()
+
+						jQuery(btnNode).dropdown()
 					}
-					if (title == '') title = label;
-					l.attr('title', title);
-					l.appendTo(_cont);
+					btnNode._initialized = true
+					return true;
+				}
 
-					if (_stateEdit) {
-						priv.addEditBtns(l);
+				global.initP5MainMenuDropdown = initP5MainMenuDropdown
+			})(window)
+		");
+
+		echo UI::h('script', [], "
+			(function (global) {
+				if (!global.p5UI__MenuStore) throw 'Missing global.p5UI__MenuStore'
+
+				function initP5UrlsMenuDropdown( btnNode, idSubMenu ) {
+					if (!btnNode._initialized) {
+						var jqDropdownTrigger = jQuery(btnNode)
+						var jqDropdownMenu = jQuery('#' + idSubMenu)
+						var jqDropdownParent = jqDropdownMenu.parent()
+						global.p5UI__MenuStore.subscribe(
+							(function (global, idSubMenu) {
+								return function renderP5MainMenuDropdown(data) {
+									var jqDropdownMenu = jQuery('#' + idSubMenu)
+									jqDropdownMenu.empty()
+									jqDropdownMenu.append(data.urls.map(function (item) {
+										var star = (-1 !== data.idsBookmarks.indexOf(item.id))
+											? '<i class=\"bookmark-item-rem glyphicon glyphicon-star\" title=\"Usuń z ulubionych\" onClick=\"return p5BookmarksRemove(event, ' + item.id + ')\"></i>'
+											: '<i class=\"bookmark-item-add glyphicon glyphicon-star-empty\" title=\"Dodaj do ulubionych\" onClick=\"return p5BookmarksAdd(event, ' + item.id + ')\"></i>'
+										return jQuery('<li>' +
+											'<a href=\"index.php?FUNCTION_INIT=URL_INIT&ZASOB_ID=' + item.id + '\" target=\"_blank\" title=\"' + item.raw_label + '\">' +
+												star +
+												' ' + '<code>' + item.id + '</code>' +
+												' ' + item.label +
+											'</a>' +
+										'</li>');
+									}))
+								}
+							})(global, idSubMenu)
+						)
+
+						jqDropdownTrigger.attr('data-toggle', 'dropdown') // is required by bootstrap dorpdown.js evenf if is called via js
+
+						global.p5UI__MenuStore.forceUpdate()
+
+						jQuery(btnNode).dropdown()
 					}
+					btnNode._initialized = true
+					return true;
 				}
 
-				// stars visibility
-				$('.bookmark-item-rem-' + item.id).show();
-				$('.bookmark-item-add-' + item.id).hide();
-			});
+				global.initP5UrlsMenuDropdown = initP5UrlsMenuDropdown
+			})(window)
+		");
 
-			if (data.length > 0) {
-				var editBtn = $('<button class="btn btn-xs" style="float:right" title="Edit Bookmarks"><i class="glyphicon glyphicon-cog"></i></button>')
-				editBtn.on('click', priv.edit);
-				editBtn.prependTo(_cont);
-			}
-		};
-
-		priv.update = function(task, zasobID, argsAdd) {
-			task = task || '';
-			zasobID = zasobID || '';
-			argsAdd = argsAdd || '';
-			$.ajax({
-				url: priv.options.url + '&_task=' + task + '&_zasobID=' + zasobID + argsAdd,
-				type: 'GET',
-				dataType: 'json',
-				contentType: "application/json; charset=utf-8",
-				data: null,
-				async: true,
-				success: function (data) {
-					$('.bookmark-item-rem').hide();
-					$('.bookmark-item-add').show();
-
-					priv.setData(data);
-				},
-				error: function (err) {
-					console.log('Error');
-					console.log(err);
-				}
-			});
-		};
+		$args = [
+			'P5MENU_URL' => Router::getRoute('P5Menu')->getLink(),
+		];
+		echo UI::h('script', [], "
+			(function (global) {
+				if (!global.p5UI__MenuStore) throw 'Missing global.p5UI__MenuStore'
+				var P5MENU_URL = '{$args['P5MENU_URL']}'
 
-		priv.changed = function(e) {
-			if (priv.options.debug) console.log(e.data);
-			if (priv.options.debug) console.log('id(' + e.data.id + ') cls(' + e.data.cls + ')');
-			priv.update('change_bookmark', e.data.id, '&btnCls=' + e.data.cls);
-			return false;
-		};
+				global.p5UI__MenuStore.setRemoteUrl(P5MENU_URL)
 
-		priv.removed = function(e) {
-			if (priv.options.debug) console.log(e.data);
-			if (priv.options.debug) console.log('id(' + e.data.id + ')');
-			priv.update('remove_bookmark', e.data.id);
-			return false;
-		};
-
-		priv.sort = function(e, ui) {
-			var idsOrder = [];
-			_cont.find('a').each(function(ind, n){
-				idsOrder.push($(n).data('id'));
-			});
-			priv.update('sort_bookmarks', 0, '&ids[]=' + idsOrder.join('&ids[]='));
-			return true;
-		};
-
-		priv.addEditBtns = function(el) {
-			var next, btn;
-			el.wrap('<div></div>');
-			next = $('<span><em> Change color:</em> </span>');
-			$.each(['btn-default', 'btn-primary', 'btn-info', 'btn-success', 'btn-warning', 'btn-danger'], function(btnInd, btnClass){
-				btn = $('<button class="btn btn-xs ' + btnClass + '"> &nbsp; </button>');
-				btn.on('click', {id: el.data('id'), cls: btnClass}, priv.changed);
-				btn.appendTo(next);
-			});
-
-			btn = $('<button class="btn btn-xs"> remove </button>');
-			btn.on('click', {id: el.data('id')}, priv.removed);
-			btn.appendTo(next);
-
-			next.insertAfter(el);
-		};
-
-		priv.edit = function(e) {
-			_stateEdit = !_stateEdit;
-			var el;
-			_cont.find('a').each(function(ind, n){
-				if (priv.options.debug) console.log(n);
-				el = $(n);
-				if (_stateEdit) {
-					priv.addEditBtns(el);
+				if (global.p5UI__MenuStore.hasData()) {
+					global.p5UI__MenuStore.forceUpdate() // force update all subscribers
 				} else {
-					el.next().remove();
-					el.unwrap();
+					global.p5UI__MenuStore.remoteUpdate() // update from remote url @see setRemoteUrl
 				}
-			});
-		}
-
-		publ.init = function(options) {
-			if (priv.options.debug) console.log('UserBookmarks initialization...');
-			//merge supplied options with defaults
-			$.extend(priv.options, defaults, options);
-			priv.init();
-			return publ;
-		};
-		return publ;
-	};
-
-	$.fn.UserBookmarks = function(options) {
-		options = options || {};
-		return this.each(function() {
-			options.id = this;
-			$(this).data('UserBookmarks', new UserBookmarks().init(options));
-		});
-	};
-})(jQuery);
-</script>
-		<script>
-		jQuery(document).ready(function(){
-			jQuery('#SE-menu-sub').UserBookmarks({
-				url: 'index-ajax.php?_cls=UserBookmarks',
-				urlInit: false,
-				preloadData: <?php echo json_encode($bookmarksJson); ?>,
-				menuid: '#SE-menu',
-				debug: false
-			});
-		});
-		</script>
-	<?php endif; ?>
-</div>
-		<?php
+			})(window)
+		");
 	}
 
 	/**

+ 119 - 0
SE/se-lib/Route/P5Menu.php

@@ -0,0 +1,119 @@
+<?php
+
+Lib::loadClass('RouteBase');
+Lib::loadClass('UserBookmarks');
+Lib::loadClass('Response');
+
+class Route_P5Menu extends RouteBase {
+
+	public function defaultAction() {
+		Response::sendTryCatchJson(array($this, 'getMenuData'), $args = 'JSON_FROM_REQUEST_BODY');
+	}
+	public function getMenuData($args = []) {
+		if ($postTask = V::get('_postTask', '', $args)) {
+			DBG::log($args, 'array', "exec '{$postTask}'");
+			if (!method_exists($this, "{$postTask}PostTask")) throw new Exception("Post Task not exists!");
+			$this->{"{$postTask}PostTask"}($args);
+		}
+
+		$userAcl = User::getAcl();
+		$listObjects = $userAcl->getTablesAcl();
+		$listUrls = $userAcl->getUrls();
+		// DBG::log($listUrls, 'array', "\$listUrls");
+
+		$bookmarks = UserBookmarks::getInstance()->getBookmarks();
+
+		return [
+			'type' => 'success',
+			'msg' => "OK",
+			'body' => [
+
+				'objects' => array_map(function ($acl, $idZasob) {
+					if (!$acl) return [
+						'id' => $idZasob,
+						'TODO' => 'TODO'
+					];
+
+					return [
+						'id' => $acl->getID(),
+						'namespace' => $acl->getNamespace(),
+						'name' => $acl->getName(),
+						'label' => $acl->getLongLabel(),
+						'raw_label' => strtolower($acl->getLongRawLabel()),
+					];
+				}, array_values($listObjects), array_keys($listObjects)),
+
+				'urls' => array_map(function ($label, $idZasob) {
+					return [
+						'id' => $idZasob,
+						'label' => (mb_strlen($label, 'utf-8') > 100) ? mb_substr($label, 0, 100, 'utf-8') . '...' : $label,
+						'raw_label' => $label,
+					];
+				}, array_values($listUrls), array_keys($listUrls)),
+
+				'bookmarks' => // array_filter(
+					array_map(function ($cls, $idZasob) use ($listObjects, $listUrls) {
+						if (array_key_exists($idZasob, $listObjects)) {
+							$acl = $listObjects[$idZasob];
+							return [
+								'id' => $idZasob,
+								'namespace' => $acl->getNamespace(),
+								'name' => $acl->getName(),
+								'label' => $acl->getRawLabel(),
+								'opis' => $acl->getOpis(),
+								'type' => 'menu',
+								'class' => $cls
+							];
+						}
+						else if (array_key_exists($idZasob, $listUrls)) {
+							return [
+								'id' => $idZasob,
+								'name' => $listUrls[$idZasob],
+								'type' => 'url',
+								'class' => $cls
+							];
+						}
+						return null;
+					}, array_values($bookmarks), array_keys($bookmarks)),
+					// , function ($bookmark) { return null !== $bookmark; }),
+
+				'idsBookmarks' => array_keys($bookmarks),
+
+			]
+		];
+	}
+
+	public function addBookmarkPostTask($args) {
+		$zasobID = V::get('_zasobID', 0, $args, 'int');
+		if ($zasobID <= 0) throw new Exception('Missing _zasobID');
+		UserBookmarks::getInstance()->addBookmark($zasobID);
+		User::saveProfile();
+	}
+
+	public function removeBookmarkPostTask($args) {
+		$zasobID = V::get('_zasobID', 0, $args, 'int');
+		if ($zasobID <= 0) throw new Exception('Missing _zasobID');
+		UserBookmarks::getInstance()->removeBookmark($zasobID);
+		User::saveProfile();
+		$this->getMenuData();
+	}
+
+	public function changeBookmarkPostTask($args) {
+		$zasobID = V::get('_zasobID', 0, $args, 'int');
+		if ($zasobID <= 0) throw new Exception('Missing _zasobID');
+		$btnCls = V::get('btnCls', '', $args);
+		if (empty($btnCls)) {
+			die('Error: no button class');
+		}
+		UserBookmarks::getInstance()->changeBookmark($zasobID, $btnCls);
+		User::saveProfile();
+	}
+
+	public function sortBookmarksPostTask($args) {
+		$idsOrdered = V::get('ids', array(), $args, 'array', array('V', 'filterPositiveInteger'));
+		if (empty($idsOrdered)) throw new Exception('Missing ids');
+		UserBookmarks::getInstance()->sortBookmarks($idsOrdered);
+		User::saveProfile();
+	}
+
+}

+ 29 - 3
SE/se-lib/Route/Storage/AclStruct.php

@@ -347,6 +347,12 @@ class Route_Storage_AclStruct extends RouteBase {
 						where t.`namespace` = '{$refNamespace}'
 					"));
 				});
+
+				$previewItems = ACL::getAclByNamespace($namespace)->buildQuery([])->getItems([
+					'limit' => 5
+				]);
+				DBG::nicePrint($previewItems, 'items (default query) - preview (limit 5)');
+
 				$query = [ 'cols' => array_merge(
 					array_map(
 						function ($field) {
@@ -359,12 +365,32 @@ class Route_Storage_AclStruct extends RouteBase {
 						}, $activeRefFields
 					)
 				) ];
-				DBG::nicePrint($query, '$query');
+				DBG::nicePrint($query, 'items with ref/* - query');
 
 				$previewItems = ACL::getAclByNamespace($namespace)->buildQuery($query)->getItems([
-					'limit' => 3
+					'limit' => 5
 				]);
-				DBG::nicePrint($previewItems, 'items with ref limit 3');
+				DBG::nicePrint($previewItems, 'items with ref/* - preview (limit 5)');
+
+				$query = [ 'cols' => array_merge(
+					array_map(
+						function ($field) {
+							return $field['fieldNamespace'];
+						}, $localFields
+					),
+					array_map(
+						function ($field) {
+							return "{$field['fieldNamespace']}";
+						}, $activeRefFields
+					)
+				) ];
+				DBG::nicePrint($query, 'items with ref as xlink - query');
+
+				$previewItems = ACL::getAclByNamespace($namespace)->buildQuery($query)->getItems([
+					'limit' => 5
+				]);
+				DBG::nicePrint($previewItems, 'items with ref as xlink - preview (limit 5)');
+
 			} catch (Exception $e) {
 				DBG::log($e);
 				UI::alert('danger', $e->getMessage());

+ 12 - 7
SE/se-lib/Route/Users.php

@@ -10,6 +10,7 @@ Lib::loadClass('TypespecialVariable');
 Lib::loadClass('TableAjax');
 Lib::loadClass('UserActivity');
 Lib::loadClass('UI');
+Lib::loadClass('Response');
 
 class Route_Users extends RouteBase {
 
@@ -89,14 +90,18 @@ class Route_Users extends RouteBase {
 
 		UI::gora();
 		UI::menu();
-		?>
-		<div class="container">
-			<div class="alert alert-success" title="<?php echo number_format($fixAllPermsExecTime, 4); ?> s / <?php echo number_format($fixUserPermsExecTime, 4); ?> s">
-				Zaktualizowano uprawnienia
-			</div>
-		</div>
-		<?php
+		echo UI::h('div', [ 'class' => "container"], [
+			UI::h('div', [
+				'class' => "alert alert-success",
+				'title' => number_format($fixAllPermsExecTime, 4) . " s / " . number_format($fixUserPermsExecTime, 4) . " s"
+			], "Zaktualizowano uprawnienia"),
+		]);
 		UI::loadTemplate('defaultPage', $data);
+		echo UI::h('script', [], "
+			(function (global) {
+				if (global.p5UI__MenuStore) global.p5UI__MenuStore.remoteUpdate()
+			})(window)
+		");
 		UI::dol();
 	}
 

+ 6 - 6
SE/se-lib/Route/ViewTableAjax.php

@@ -468,21 +468,21 @@ class Route_ViewTableAjax extends RouteBase {
 				if (!empty($tsValues[$kID])) $fieldParams['typespecialValue'] = $tsValues[$kID];
 
 				$jsFields[] = [ 'div', [ 'class' => "form-group" ], [
-					[ 'label', [ 'class' => "col-xs-12 col-sm-3 col-md-2 control-label", 'for' => "f{$kID}" ], [
+					[ 'label', [ 'class' => "control-label", 'for' => "f{$kID}" ], [
 						[ 'span', [ 'style' => ['padding-right'=>'4px'] ], $vCol['label'] ],
 						[ 'i', [ 'class' => "glyphicon glyphicon-info-sign frm-help", 'data-toggle' => "popover", 'data-trigger' => "hover", 'title' => "", 'data-content' => htmlspecialchars($vCol['opis']), 'data-original-title' => "[{$kID}] {$fieldName}" ] ],
 					] ],
-					[ 'div', [ 'class' => "col-xs-12 col-sm-9 col-md-10" ], [
+					[ 'div', [ 'class' => "" ], [
 						UI::hGetFormItem($acl, $fieldName, 'W', $kID, "f{$kID}", $cols[$kID], $fieldParams, $record),
 					] ]
 				] ];
 			} else if ($acl->canReadObjectField($fieldName, $record)) {
 				$jsFields[] = [ 'div', [ 'class' => "form-group" ], [
-					[ 'label', [ 'class' => "col-xs-12 col-sm-3 col-md-2 control-label", 'for' => "f{$kID}" ], [
+					[ 'label', [ 'class' => "control-label", 'for' => "f{$kID}" ], [
 						[ 'span', [ 'style' => ['padding-right'=>'4px'] ], $vCol['label']],
 						[ 'i', [ 'class' => "glyphicon glyphicon-info-sign frm-help", 'data-toggle' => "popover", 'data-trigger' => "hover", 'title' => "", 'data-content' => htmlspecialchars($vCol['opis']), 'data-original-title' => "[{$kID}] {$fieldName}" ] ],
 					] ],
-					[ 'div', [ 'class' => "col-xs-12 col-sm-9 col-md-10" ], [
+					[ 'div', [ 'class' => "" ], [
 						['p', [ 'style' => [ 'margin-top' => '5px' ] ], [
 							(!empty($tsValues[$kID]))
 								? $tsValues[$kID]
@@ -497,7 +497,7 @@ class Route_ViewTableAjax extends RouteBase {
 			}
 		}
 		$jsFields[] = [ 'div', [ 'class' => "form-group" ], [
-			[ 'div', [ 'class' => "col-xs-offset-0 col-xs-12 col-sm-offset-3 col-sm-9 col-md-offset-2 col-md-10" ], [
+			[ 'div', [ 'class' => "" ], [
 				['button', [ 'type' => "submit", 'class' => "btn btn-primary", 'tabindex' => ++$tabindex ], "Zapisz" ]
 			] ]
 		] ];
@@ -520,7 +520,7 @@ class Route_ViewTableAjax extends RouteBase {
 					[ 'small', [ 'class' => "pull-right valign-btns-bottom" ], [ $rowFunctionsOut ] ],
 				] ],
 				[ 'P5UI__FeatureEditForm', [
-					'class' => "form-horizontal", 'action' => "", 'method' => "post",
+					'class' => "", 'action' => "", 'method' => "post",
 					'id' => "EDIT_FRM_{$this->_htmlID}", // TODO: rm - use React nodes // TODO: $this->_htmlID not exists!
 					'ajaxSaveUrl' => "{$syncUrl}&_task=editSaveAjax", // TODO:? &_hash={$this->_htmlID}
 					'namespace' => $acl->getNamespace(),

+ 1 - 1
SE/se-lib/Schema/SystemObjectStorageAcl.php

@@ -221,7 +221,7 @@ class Schema_SystemObjectStorageAcl extends Core_AclSimpleSchemaBase {
     }
 
     // // foreach ... DB::getPDO($idDatabase)->fetchAll(select real _rootTableName)
-    // foreach (Core_AclHelper::getAclList() as $typeName) {
+    // foreach (Core_AclHelper::getCustomAclList() as $typeName) {
     //   $ns = Core_AclHelper::parseTypeName($typeName);
     //   $namespace = str_replace('__x3A__', '/', $ns['prefix']) . "/{$ns['name']}";
     //   $sqlNs = DB::getPDO()->quote($namespace, PDO::PARAM_STR);

+ 48 - 6
SE/se-lib/TableAcl.php

@@ -6,6 +6,7 @@ Lib::loadClass('Typespecial');
 Lib::loadClass('ProcesHelper');
 Lib::loadClass('Schema_TableFactory');
 Lib::loadClass('DataSourceFactory');
+Lib::loadClass('SchemaFactory');
 
 /**
  * $_SESSION['TableAcl_cache'][$tableID] = array(
@@ -179,7 +180,13 @@ class TableAcl extends Core_AclBase {
 
 	public function getFields() {
 		$this->fieldsInit();
-		return $this->_fields;
+		$types = $this->_types;
+		return array_map(function ($field) use ($types) {
+			return array_merge($field, [
+				'fieldNamespace' => $field['name'],
+				'isLocal' => array_key_exists($field['name'], $types),
+			]);
+		}, $this->_fields);
 	}
 
 	public function setFieldPerms($fieldID, $perms) {
@@ -917,16 +924,44 @@ class TableAcl extends Core_AclBase {
 		if (array_key_exists($idTable, $_cache)) {
 			return $_cache[$idTable];
 		}
+
 		if (!empty($_SESSION['TableAcl_cache'][$idTable])) {
 			$tableAcl = new TableAcl($idTable);
 			$tableAcl->fromArray($_SESSION['TableAcl_cache'][$idTable]);
 			$_cache[$idTable] = $tableAcl;
 			return $_cache[$idTable];
 		}
+		if (!empty($_SESSION['AntAcl_cache'][$idTable])) {
+			try {
+				$objectList = SchemaFactory::loadDefaultObject('SystemObject')->getItems([
+					'propertyName' => "*,field",
+					'f_idZasob' => "={$idTable}",
+				]);
+				if (empty($objectList)) throw new Exception("Acl [{$idTable}] not exists in SystemObject"); // TODO: update Storage object list?
+
+				$objItem = reset($objectList);
+				DBG::log($objItem, 'array', "DBG objItem({$namespace})");
+				if (!in_array($objItem['_type'], [
+					// 'TableAcl', // TODO: TEST - to replace TableAcl by AntAcl or use object with namespace + '/tableName'?
+					'AntAcl',
+				])) throw new Exception("Not Implemented acl type '{$objItem['_type']}'");
+				if (!$objItem['isObjectActive']) {
+					if (!$objItem['hasStruct']) throw new Exception("namespace has no structure '{$namespace}'");
+					if (!$objItem['isStructInstalled']) throw new Exception("namespace structure not installed '{$namespace}'");
+					throw new Exception("namespace is not activated '{$namespace}'");
+				}
+
+				Lib::loadClass('AntAclBase');
+				$_cache[$idTable] = AntAclBase::buildInstance($objItem['idZasob'], $objItem);
+				return $_cache[$idTable];
+			} catch (Exception $e) {
+				DBG::log($e);
+			}
+		}
 		return null;
 	}
 
-	public static function buildInstance($idTable, $tableConfig) {
+	public static function buildInstance($idTable, $tableConfig) { // TODO: move outside - app session cache?
 		static $_cache;
 		if (!$_cache) $_cache = array();
 		if (array_key_exists($idTable, $_cache)) {
@@ -959,10 +994,17 @@ class TableAcl extends Core_AclBase {
 		// 	}
 		// }
 		if (empty($tableConfig)) throw new Exception("Brak danych konfiguracyjnych do tabeli nr {$idTable} #TACL" . __LINE__);
-		$obj = new TableAcl($idTable);
-		$obj->fromArray($tableConfig);
-		$obj->getRootTableName();
-		$obj->save();
+
+		$type = (false !== strpos($tableConfig['name'], '/')) ? 'AntAcl' : 'TableAcl'; // TODO: fix naive check this // $type = DB::getPDO()->fetchValue(" select o._type from `CRM_#CACHE_ACL_OBJECT` o where o.idZasob = '{$idTable}' limit 1 ");
+		if ('AntAcl' === $type) {
+			DBG::log("AntAcl save config in TableAcl::buildInstance [{$idTable}] '{$tableConfig['name']}'");
+			$_SESSION['AntAcl_cache'][$idTable] = true;
+		} else { // TableAcl
+			$obj = new TableAcl($idTable);
+			$obj->fromArray($tableConfig);
+			$obj->getRootTableName();
+			$obj->save();
+		}
 		$_cache[$idTable] = $obj;
 		return $_cache[$idTable];
 	}

+ 11 - 11
SE/se-lib/TableAjax.php

@@ -2132,27 +2132,27 @@ var p5UI_TableAjax_generateFunctionNode = function(funObj, rowPK, props) {
 			if (Object.keys(_state.filters.filterCols).length > 0) {
 				$.each(_state.filters.filterCols, function(col, colProps) {
 					if (colProps.filter && colProps.filter.length > 0) {
-						initUrlAdd += '&f_' + col + '=' + colProps.filter;
+						initUrlAdd += '&f_' + col + '=' + encodeURIComponent(colProps.filter);
 						filtersInitSet = true;
 					}
 				});
 			}
 			$.each(_state.specialFilters, function(groupName, btnValue) {
 				if (btnValue.length > 0) {
-					initUrlAdd += '&sf_' + groupName + '=' + btnValue;
+					initUrlAdd += '&sf_' + groupName + '=' + encodeURIComponent(btnValue);
 					filtersInitSet = true;
 				}
 			});
 
 			if (priv.options.forceFilterInit) {
 				$.map(priv.options.forceFilterInit, function(fltrProps, fltr) {
-					initUrlAdd += '&f_' + fltr + '=' + fltrProps;
+					initUrlAdd += '&f_' + fltr + '=' + encodeURIComponent(fltrProps);
 					filtersInitSet = true;
 				});
 			}
 
 			// p5UI__notifyAjaxCallback({type: 'info', msg: 'pobieranie danych (init) ...'});
-			fetch(priv.options.url + initUrlAdd, {
+			window.fetch(priv.options.url + initUrlAdd, {
 			  method: priv.options.urlPost ? 'POST' : 'GET',
 				credentials: 'same-origin',// add cookies
 			}).then(function (response) {
@@ -2870,7 +2870,7 @@ var p5UI_TableAjax_generateFunctionNode = function(funObj, rowPK, props) {
 				_state._modelColFilter.saveBtn.on('click', priv.modelColFilter_saveBtnClicked)
 
 				if (priv.options.userTableFilterUrl) {
-					fetch(priv.options.userTableFilterUrl, {
+					window.fetch(priv.options.userTableFilterUrl, {
 						method: 'POST',
 						headers: {
 							'Content-Type': 'application/json'
@@ -2908,7 +2908,7 @@ var p5UI_TableAjax_generateFunctionNode = function(funObj, rowPK, props) {
 			e.stopPropagation()
 			var filtrName = $(this).data('col_filter')
 			if (!filtrName) return
-			fetch('<?= Request::getPathUri() . "index.php?_route=ViewTableAjax&_task=rmUserTableFilterAjax" ?>', {
+			window.fetch('<?= Request::getPathUri() . "index.php?_route=ViewTableAjax&_task=rmUserTableFilterAjax" ?>', {
 				method: 'POST',
 				headers: {
 					'Content-Type': 'application/json'
@@ -2958,7 +2958,7 @@ var p5UI_TableAjax_generateFunctionNode = function(funObj, rowPK, props) {
 					return new Promise(function(resolve, reject) {
 						if (!filtrName) reject('Proszę podać nazwę filtra')
 						if (filtrName.length > 255) reject('Nazwa za długa')
-						fetch('<?= Request::getPathUri() . "index.php?_route=ViewTableAjax&_task=addUserTableFilterAjax" ?>', {
+						window.fetch('<?= Request::getPathUri() . "index.php?_route=ViewTableAjax&_task=addUserTableFilterAjax" ?>', {
 							method: 'POST',
 							headers: {
 								'Content-Type': 'application/json'
@@ -3403,7 +3403,7 @@ var p5UI_TableAjax_generateFunctionNode = function(funObj, rowPK, props) {
 			if (Object.keys(_state.filters.filterCols).length > 0) {
 				$.each(_state.filters.filterCols, function(col, colProps) {
 					if (colProps.filter && colProps.filter.length > 0) {
-						urlAdd += '&f_' + col + '=' + colProps.filter;
+						urlAdd += '&f_' + col + '=' + encodeURIComponent(colProps.filter);
 					}
 				});
 				skipCols = true;
@@ -3412,13 +3412,13 @@ var p5UI_TableAjax_generateFunctionNode = function(funObj, rowPK, props) {
 			// specialFilters
 			$.each(_state.specialFilters, function(groupName, btnValue) {
 				if (btnValue.length > 0) {
-					urlAdd += '&sf_' + groupName + '=' + btnValue;
+					urlAdd += '&sf_' + groupName + '=' + encodeURIComponent(btnValue);
 				}
 			});
 
 			if (priv.options.forceFilterInit) {
 				$.map(priv.options.forceFilterInit, function(fltrProps, fltr) {
-					urlAdd += '&f_' + fltr + '=' + fltrProps;
+					urlAdd += '&f_' + fltr + '=' + encodeURIComponent(fltrProps);
 					filtersInitSet = true;
 				});
 			}
@@ -3426,7 +3426,7 @@ var p5UI_TableAjax_generateFunctionNode = function(funObj, rowPK, props) {
 			_uiNode$Table.parent().parent().addClass('AjaxTable-loading');
 
 			// p5UI__notifyAjaxCallback({type: 'info', msg: 'pobieranie danych...'});
-			fetch(priv.options.url + urlAdd, {
+			window.fetch(priv.options.url + urlAdd, {
 			  method: priv.options.urlPost ? 'POST' : 'GET',
 				credentials: 'same-origin',// add cookies
 			}).then(function (response) {

+ 30 - 7
SE/se-lib/User.php

@@ -245,6 +245,12 @@ class User {
 							Lib::loadClass('UI');
 							UI::gora();
 							UI::loadTemplate('logout', $data);
+							UI::inlineJS(APP_PATH_WWW . '/static/p5UI/menuStore.js');
+							echo UI::h('script', [], "
+								(function (global) {
+									if (global.p5UI__MenuStore) global.p5UI__MenuStore.clearCache()
+								})(window)
+							");
 							UI::dol();
 							exit;
 						}
@@ -264,6 +270,12 @@ class User {
 					Lib::loadClass('UI');
 					UI::gora();
 					UI::loadTemplate('logout', $data);
+					UI::inlineJS(APP_PATH_WWW . '/static/p5UI/menuStore.js');
+					echo UI::h('script', [], "
+						(function (global) {
+							if (global.p5UI__MenuStore) global.p5UI__MenuStore.clearCache()
+						})(window)
+					");
 					UI::dol();
 					exit;
 				}
@@ -295,13 +307,18 @@ class User {
 
 					UI::gora();
 					UI::menu();
-					?>
-					<div class="container">
-						<div class="alert alert-success" title="<?php echo number_format($fixAllPermsExecTime, 4); ?> s / <?php echo number_format($fixUserPermsExecTime, 4); ?> s">
-							Zaktualizowano uprawnienia
-						</div>
-					</div>
-					<?php
+					echo UI::h('div', [ 'class' => "container"], [
+						UI::h('div', [
+							'class' => "alert alert-success",
+							'title' => number_format($fixAllPermsExecTime, 4) . " s / " . number_format($fixUserPermsExecTime, 4) . " s"
+						], "Zaktualizowano uprawnienia"),
+					]);
+					UI::loadTemplate('defaultPage', $data);
+					echo UI::h('script', [], "
+						(function (global) {
+							if (global.p5UI__MenuStore) global.p5UI__MenuStore.remoteUpdate()
+						})(window)
+					");
 					UI::loadTemplate('defaultPage', $data);
 					UI::dol();
 					exit;
@@ -361,6 +378,12 @@ class User {
 			Lib::loadClass('UI');
 			UI::gora();
 			UI::loadTemplate('login', $data);
+			UI::inlineJS(APP_PATH_WWW . '/static/p5UI/menuStore.js');
+			echo UI::h('script', [], "
+				(function (global) {
+					if (global.p5UI__MenuStore) global.p5UI__MenuStore.clearCache()
+				})(window)
+			");
 			UI::dol();
 			exit;
 		}

+ 2 - 2
SE/se-lib/UserAcl.php

@@ -152,7 +152,7 @@ class UserAcl {
 			$tblIds = array();
 			foreach ($value as $idTable => $tableConfig) {
 				$tblIds[] = $idTable;
-				$vTableAcl = TableAcl::buildInstance($idTable, $tableConfig);
+				TableAcl::buildInstance($idTable, $tableConfig);
 			}
 			$value = $tblIds;
 		}
@@ -207,7 +207,7 @@ class UserAcl {
 
 	public function getTableAcl($tableID) {
 		$tblAcl = TableAcl::getInstance($tableID);
-		if (!$tblAcl) throw new Exception("Brak tabeli nr [{$tableID}]!");
+		if (!$tblAcl) throw new Exception("Brak obiektu nr [{$tableID}]!");
 		$tblAcl->init();
 		return $tblAcl;
 	}

+ 115 - 0
SE/static/p5UI/menuStore.js

@@ -0,0 +1,115 @@
+function p5UI__MenuStore() {
+
+  var _url = ''
+  var _subscribers = []
+
+  function _subscribe( fn ) { // fn( { bookmarks, idsBookmarks, objects, urls } )
+    _subscribers.push( fn )
+  }
+
+  function _update( data ) { // data: { bookmarks, idsBookmarks, objects, urls }
+
+    data.objects.sort(function (a, b) {
+      if (a.raw_label > b.raw_label) return 1
+      if (a.raw_label < b.raw_label) return -1
+      return 0
+    })
+    data.urls.sort(function (a, b) {
+      if (a.raw_label > b.raw_label) return 1
+      if (a.raw_label < b.raw_label) return -1
+      return 0
+    })
+
+    _set('bookmarks', data.bookmarks)
+    _set('idsBookmarks', data.idsBookmarks)
+    _set('objects', data.objects)
+    _set('urls', data.urls)
+
+    var data = _getData()
+    _subscribers.forEach(function (fn) {
+      fn(data)
+    })
+  }
+
+  function _forceUpdate() {
+    var data = _getData()
+    _subscribers.forEach(function (fn) {
+      fn(data)
+    })
+  }
+
+  function _getData() {
+    return {
+      bookmarks: _get('bookmarks'),
+      idsBookmarks: _get('idsBookmarks'),
+      objects: _get('objects'),
+      urls: _get('urls')
+    }
+  }
+
+  function _hasData() {
+    var data = _getData()
+    return (
+      null !== data.bookmarks
+      || null !== data.idsBookmarks
+      || null !== data.objects
+      || null !== data.urls
+    )
+  }
+
+  function _set(type, value) {
+    global.localStorage.setItem('p5Menu.' + type, JSON.stringify(value))
+  }
+
+  function _get(type, value) {
+    var val = global.localStorage.getItem('p5Menu.' + type)
+    return val ? JSON.parse(val) : null
+  }
+
+  function _setRemoteUrl(url) {
+    _url = url
+  }
+
+  function _remoteUpdate(postData) {
+    var options =
+    window.fetch(_url
+      , (!postData)
+        ? { method: 'GET',
+            headers: { 'Content-Type': 'application/json' },
+            credentials: 'same-origin',
+          }
+        : { method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            credentials: 'same-origin',
+            body: JSON.stringify(postData)
+          }
+    ).then(function (response) {
+      return response.json()
+    }).then(function (response) {
+      if ('success' === response.type) {
+        _update(response.body)
+      } else {
+        // err...
+      }
+    })
+  }
+
+  function _clearCache() {
+    global.localStorage.removeItem('p5Menu.' + 'urls')
+    global.localStorage.removeItem('p5Menu.' + 'objects')
+    global.localStorage.removeItem('p5Menu.' + 'bookmarks')
+    global.localStorage.removeItem('p5Menu.' + 'idsBookmarks')
+  }
+
+  return {
+    subscribe: _subscribe,
+    update: _update,
+    forceUpdate: _forceUpdate,
+    setRemoteUrl: _setRemoteUrl,
+    remoteUpdate: _remoteUpdate,
+    getData: _getData,
+    hasData: _hasData,
+    clearCache: _clearCache,
+  }
+}
+global.p5UI__MenuStore = p5UI__MenuStore()

+ 169 - 0
SE/static/p5UI/userBookmarks.js

@@ -0,0 +1,169 @@
+var $ = global.jQuery
+var UserBookmarks = function() {
+  var priv = {}; //private api
+  var publ = {}; //public api
+
+  priv.options = {};
+  var defaults = {
+    url: '',  //webservice url
+    store: null, // @required
+    debug: false
+  };
+
+  var _cont; // container holding table
+  var _stateEdit = false;
+
+  /*
+   initialize the plugin.
+   */
+  priv.init = function() {
+    _cont = $(priv.options.id);
+
+    priv.options.store.subscribe(
+      (function (global, priv) {
+        return function renderUserBookmarks(data) {
+          priv.setData(data.bookmarks)
+        }
+      })(global, priv)
+    )
+
+    _cont.sortable();
+    _cont.on('sortupdate', priv.sort);
+  };
+
+  priv.setData = function(data) {
+    _cont.empty();
+    data.forEach(function(item) {
+      if (!item) return;
+      if ('type' in item) {
+        var l = $('<a></a>');
+        l.data('id', item.id);
+        l.addClass('btn');
+        l.addClass('btn-xs');
+        var label = item.name, title = '';
+        if (item.hasOwnProperty('class') && item['class'] != '') {
+          l.addClass(item.class);
+        } else {
+          l.addClass('btn-default');
+        }
+        if (item.type == 'menu') {
+          // l.attr('href', 'index.php?_route=ViewTableAjax&typeName=' + 'p5_default_db:' + item.name);
+          l.attr('href', 'index.php?_route=ViewTableAjax&namespace=' + item.namespace);
+          if ('label' in item && item.label.length > 0) {
+            label = item.label;
+            title = item.label + ' (' + item.name + ')';
+          }
+          else if ('opis' in item && item.opis.length > 0) {
+            label = item.opis;
+            title = item.opis + ' (' + item.name + ')';
+          }
+        } else if (item.type == 'url') {
+          l.attr('href', 'index.php?FUNCTION_INIT=URL_INIT&ZASOB_ID=' + item.id);
+          l.attr('target', '_blank');
+        }
+        if (label.length > 20) {
+          var pos = label.indexOf(' - ');
+          if (pos > 20 || pos < 5) {
+            pos = 20;
+            l.text(label.substring(0, pos) + ' ...');
+          } else {
+            l.text(label.substring(0, pos));
+          }
+        } else {
+          l.text(label);
+        }
+        if (title == '') title = label;
+        l.attr('title', title);
+        l.appendTo(_cont);
+
+        if (_stateEdit) {
+          priv.addEditBtns(l);
+        }
+      }
+    });
+
+    if (data.length > 0) {
+      var editBtn = $('<button class="btn btn-xs" style="float:right" title="Edit Bookmarks"><i class="glyphicon glyphicon-cog"></i></button>')
+      editBtn.on('click', priv.edit);
+      editBtn.prependTo(_cont);
+    }
+  };
+
+  priv.changed = function(e) {
+    global.p5UI__MenuStore.remoteUpdate({
+      '_postTask': 'changeBookmark',
+      '_zasobID': e.data.id,
+      btnCls: e.data.cls
+    })
+    return false;
+  };
+
+  priv.removed = function(e) {
+    global.p5UI__MenuStore.remoteUpdate({
+      '_postTask': 'removeBookmark',
+      '_zasobID': e.data.id,
+    })
+    return false;
+  };
+
+  priv.sort = function(e, ui) {
+    var idsOrder = _cont.find('a').map(function(ind, n){
+      return $(n).data('id');
+    });
+    global.p5UI__MenuStore.remoteUpdate({
+      '_postTask': 'sortBookmarks',
+      '_zasobID': 0,
+      ids: idsOrder,
+    })
+    return true;
+  };
+
+  priv.addEditBtns = function(el) {
+    var next, btn;
+    el.wrap('<div></div>');
+    next = $('<span><em> Change color:</em> </span>');
+    $.each(['btn-default', 'btn-primary', 'btn-info', 'btn-success', 'btn-warning', 'btn-danger'], function(btnInd, btnClass){
+      btn = $('<button class="btn btn-xs ' + btnClass + '"> &nbsp; </button>');
+      btn.on('click', {id: el.data('id'), cls: btnClass}, priv.changed);
+      btn.appendTo(next);
+    });
+
+    btn = $('<button class="btn btn-xs"> remove </button>');
+    btn.on('click', {id: el.data('id')}, priv.removed);
+    btn.appendTo(next);
+
+    next.insertAfter(el);
+  };
+
+  priv.edit = function(e) {
+    _stateEdit = !_stateEdit;
+    var el;
+    _cont.find('a').each(function(ind, n){
+      if (priv.options.debug) console.log(n);
+      el = $(n);
+      if (_stateEdit) {
+        priv.addEditBtns(el);
+      } else {
+        el.next().remove();
+        el.unwrap();
+      }
+    });
+  }
+
+  publ.init = function(options) {
+    if (priv.options.debug) console.log('UserBookmarks initialization...');
+    //merge supplied options with defaults
+    $.extend(priv.options, defaults, options);
+    priv.init();
+    return publ;
+  };
+  return publ;
+};
+
+$.fn.UserBookmarks = function(options) {
+  options = options || {};
+  return this.each(function() {
+    options.id = this;
+    $(this).data('UserBookmarks', new UserBookmarks().init(options));
+  });
+};