Browse Source

depot git type

idleman 4 years ago
parent
commit
09cfb70448
100 changed files with 8627 additions and 1227 deletions
  1. 5 1
      .htaccess
  2. 0 1
      account.lost.php
  3. 2 3
      account.php
  4. 319 289
      action.php
  5. 1 0
      class/Action.class.php
  6. 188 0
      class/Api.class.php
  7. 11 4
      class/Configuration.class.php
  8. 44 2
      class/Entity.class.php
  9. 13 2
      class/Excel.class.php
  10. 31 13
      class/File.class.php
  11. 3 3
      class/Image.class.php
  12. 38 1
      class/Log.class.php
  13. 1 1
      class/Mail.class.php
  14. 32 33
      class/Pdf.class.php
  15. 4 4
      class/Plugin.class.php
  16. 1 1
      class/Right.class.php
  17. 71 68
      class/User.class.php
  18. 25 21
      common.php
  19. 16 0
      connector/Mysql.class.php
  20. 18 0
      connector/Sqlite.class.php
  21. 9 5
      constant-sample.php
  22. 3 3
      css/bootstrap.min.css
  23. 0 0
      css/bootstrap.min.css.map
  24. 208 51
      css/main.css
  25. 1 3
      firm.php
  26. BIN
      fonts/lato-light-latin-ext.woff2
  27. BIN
      fonts/lato-light-latin.woff2
  28. BIN
      fonts/lato-regular-latin-ext.woff2
  29. BIN
      fonts/lato-regular-latin.woff2
  30. 163 150
      footer.php
  31. 135 111
      function.php
  32. 19 13
      header.php
  33. 106 0
      img/logo.svg
  34. BIN
      img/logo/default-favicon.png
  35. BIN
      img/logo/default-logo.dark.png
  36. BIN
      img/logo/default-logo.png
  37. 22 17
      install.php
  38. 1 1
      maintenance.php
  39. 58 0
      plugin/customiser/theme/hackpoint/main.css
  40. 519 0
      plugin/document/Element.class.php
  41. 26 0
      plugin/document/ElementRight.class.php
  42. 676 0
      plugin/document/WebDav.class.php
  43. 551 0
      plugin/document/action.php
  44. 12 0
      plugin/document/app.json
  45. 885 0
      plugin/document/css/document.api.css
  46. 7 0
      plugin/document/css/main.css
  47. 3 0
      plugin/document/css/widget.css
  48. 349 0
      plugin/document/document.plugin.php
  49. 4 0
      plugin/document/img/breadcrumb.svg
  50. 24 0
      plugin/document/img/default-image.svg
  51. 91 0
      plugin/document/img/file-types/access.svg
  52. 21 0
      plugin/document/img/file-types/camera.svg
  53. 14 0
      plugin/document/img/file-types/code.svg
  54. 15 0
      plugin/document/img/file-types/contact.svg
  55. 19 0
      plugin/document/img/file-types/document.svg
  56. 13 0
      plugin/document/img/file-types/download.svg
  57. 109 0
      plugin/document/img/file-types/excel.svg
  58. 18 0
      plugin/document/img/file-types/feed.svg
  59. 9 0
      plugin/document/img/file-types/folder.svg
  60. 49 0
      plugin/document/img/file-types/gear.svg
  61. 24 0
      plugin/document/img/file-types/image.svg
  62. 18 0
      plugin/document/img/file-types/information.svg
  63. 20 0
      plugin/document/img/file-types/mail.svg
  64. 16 0
      plugin/document/img/file-types/message.svg
  65. 23 0
      plugin/document/img/file-types/microphone.svg
  66. 16 0
      plugin/document/img/file-types/music.svg
  67. 62 0
      plugin/document/img/file-types/outlook.svg
  68. 70 0
      plugin/document/img/file-types/pdf.svg
  69. 17 0
      plugin/document/img/file-types/phone.svg
  70. 56 0
      plugin/document/img/file-types/photoshop.svg
  71. 79 0
      plugin/document/img/file-types/powerpoint.svg
  72. 12 0
      plugin/document/img/file-types/stats.svg
  73. 21 0
      plugin/document/img/file-types/url.svg
  74. 82 0
      plugin/document/img/file-types/word.svg
  75. 13 0
      plugin/document/js/component.js
  76. 1530 0
      plugin/document/js/document.api.js
  77. 34 0
      plugin/document/js/main.js
  78. 17 0
      plugin/document/js/widget.js
  79. 8 0
      plugin/document/page.list.php
  80. 13 0
      plugin/document/setting.document.php
  81. 184 0
      plugin/document/template.document.php
  82. 3 0
      plugin/document/todo.txt
  83. 23 0
      plugin/document/widget.configure.php
  84. 25 0
      plugin/document/widget.php
  85. 22 0
      plugin/hackpoint/action.php
  86. 2 0
      plugin/hackpoint/js/main.js
  87. 85 0
      plugin/hackpoint/types/GitType.class.php
  88. 62 2
      plugin/navigation/MenuItem.class.php
  89. 177 85
      plugin/navigation/action.php
  90. 4 4
      plugin/navigation/app.json
  91. 165 61
      plugin/navigation/css/main.css
  92. 41 0
      plugin/navigation/css/widget.css
  93. 175 100
      plugin/navigation/js/main.js
  94. 45 0
      plugin/navigation/js/widget.js
  95. 194 133
      plugin/navigation/navigation.plugin.php
  96. 76 0
      plugin/navigation/page.iframe.php
  97. 141 2
      plugin/navigation/setting.global.navigation.php
  98. 10 0
      plugin/navigation/widget.configure.php
  99. 25 0
      plugin/navigation/widget.php
  100. 75 39
      plugin/notification/Notification.class.php

+ 5 - 1
.htaccess

@@ -2,7 +2,11 @@
 
 Options +FollowSymlinks
 RewriteEngine On
-RewriteRule ^api/(.*)$ action.php?action=api&command=$1 [L]
+
+
+ 
+RewriteRule ^api/(.*)$ action.php?action=api&command=$1 [QSA]
+
 
 #if requested resource isn't a file
 # and isn't a directory

+ 0 - 1
account.lost.php

@@ -48,7 +48,6 @@ $page = basename($_SERVER['PHP_SELF']);
 				}
 			}
 			if(!$myUser->connected()) throw new Exception("Erreur lors de la récuperation du compte, veuillez contacter un administrateur");
-			
 
 			?>
 			<h6>Bienvenue <?php echo $myUser->fullName(); ?>!</h6>

+ 2 - 3
account.php

@@ -1,6 +1,6 @@
 <?php require_once __DIR__.DIRECTORY_SEPARATOR.'header.php';  
 
-if(!$myUser->connected()) throw new Exception("Vous devez être connecté pour accéder à cette fonctionnalité",401);
+User::check_access('account','read');
 
 $accountMenu = array();
 Plugin::callHook("menu_account", array(&$accountMenu));
@@ -18,7 +18,7 @@ $page = basename($_SERVER['PHP_SELF']);
 		<div class="list-group">
 			<h5 class="list-group-item">Préférences</h5>
 			<?php foreach($accountMenu as $item): ?>
-				<a href="<?php echo $item['url']; ?>" class="list-group-item <?php echo $item['url'] == $page.'?section='.$_['section']?'active':'' ?> "><i class="<?php echo $item['icon']; ?>"></i> <?php echo $item['label']; ?></a>
+				<a href="<?php echo $item['url']; ?>" class="list-group-item list-group-item-action <?php echo $item['url'] == $page.'?section='.$_['section']?'active':'text-primary'; ?>"><i class="<?php echo $item['icon']; ?>"></i> <?php echo $item['label']; ?></a>
 			<?php endforeach; ?>
 		</div>
 	</div>
@@ -27,5 +27,4 @@ $page = basename($_SERVER['PHP_SELF']);
 	</div>
 </div>
 
-
 <?php require_once __ROOT__.'footer.php' ?>

File diff suppressed because it is too large
+ 319 - 289
action.php


+ 1 - 0
class/Action.class.php

@@ -42,6 +42,7 @@ class Action
             
         } catch (Exception $e) {
             $response['error'] = $e->getMessage();
+            $response['errorCode'] = $e->getCode();
             if($myUser->superadmin) $response['trace'] = $e->getTraceAsString();
         }
 

+ 188 - 0
class/Api.class.php

@@ -0,0 +1,188 @@
+<?php
+/**
+ * Classe de gestion des API (REST).
+ * @author Valentin CARRUESCO
+ * @category Plugin
+ * @license copyright
+ */
+
+
+class Api{
+	public $slug,$description;
+
+	function __construct($slug=null,$description=null){
+		if(isset($slug)) $this->slug = $slug;
+		if(isset($description)) $this->description = $description;
+		$this->route = array();
+		return $this;
+	}
+
+	function route($slug = null,$description = null,$method = null,$callback){
+		$route =  new ApiRoute($slug,$description,$method,$callback);
+		$this->route[$route->slug][$method] = $route;
+	}
+
+	function register(){
+		Plugin::addHook('api',function(&$apis){
+			$apis[$this->slug] = $this;
+			
+		});
+	}
+
+	public static function run(){
+		global $_,$myUser;
+		$response = array();
+		try{
+
+
+			/*set_error_handler(function( $errno ,  $errstr , $errfile ,  $errline){
+				throw new Exception("Error ".$errno." Processing Request ".$errfile.$errstr, 500);
+			});
+			register_shutdown_function(function( $errno ,  $errstr , $errfile ,  $errline){
+				$error = error_get_last();
+				throw new Exception("Error ".E_CORE_ERROR." Processing Request ".$error['file'], 500);
+			});*/
+
+			$command = explode('/',$_['command']);
+			if(count($command)<1) throw new Exception("Unspecified API");
+
+			$headers = apache_request_headers();
+
+			
+			//basic auth
+			if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])){
+				$user = User::check($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']);
+				if(!$user) throw new Exception('Le compte spécifié est inexistant');
+				$myUser = $user;
+			}
+
+
+			$request = array(
+				'version' =>array_shift($command),
+				'module' =>array_shift($command),
+				'route' =>array_shift($command),
+				'body' =>file_get_contents("php://input"),
+				'method' => strtoupper($_SERVER['REQUEST_METHOD']),
+				'parameters' => $_,
+				'pathes' => array_filter($command)
+			);
+
+			
+
+			$code = 200;
+			$headers = array(
+				'Content-Type: application/json'
+			);
+			$apis = array();
+			Plugin::callHook('api',array(&$apis));
+			
+			if(!isset($request['version']) || !is_numeric($request['version'])) throw new Exception("Api version is missing or invalid, specify ".ROOT_URL."/api/{version} for valid requests", 404);
+
+			if($request['module'] == 'schema'){
+				foreach ($apis as $api) {
+					$routes= array();
+					foreach($api->route as $route){
+						
+						foreach($route as $method=>$infos){
+							if(!isset($routes[$infos->slug])) $routes[$infos->slug] = array() ;
+							$routes[$infos->slug][] = $infos->method.' '.$api->slug .'/'.$infos->pattern .': '. $infos->description ;
+						}
+					}
+					$response[] =  array( $api->slug => $api->description,'routes'=> $routes);
+				}
+			}else{
+				if(!isset($apis[$request['module']])) throw new Exception("Api '".$request['module']."' is missing, see ".ROOT_URL."/api/schema for available calls", 404);
+				$api = $apis[$request['module']];
+				
+				
+				if(!isset($api->route[$request['route']]))  throw new Exception("Route '".$api->slug.'/'.$request['route']."' is missing, see ".ROOT_URL."/api/schema?pretty for available calls", 404);
+				$route = $api->route[$request['route']];
+
+
+				
+
+				if(!isset($route[$request['method']]))  throw new Exception("Method ".$request['method']." '".$api->slug.'/'.$route->slug."' is not allowed", 405);
+				$method = $route[$request['method']];
+				$callback = $method->callback;
+
+				
+				$callback($request,$response);
+
+
+
+
+				if(isset($response['code'])) $code = $response['code'];
+				if(isset($response['headers'])) $headers = array_merge($headers,$response['headers']);
+
+
+
+				unset($response['code']);
+				unset($response['headers']);
+			}
+
+			
+
+		}catch(Exception $e){
+			$response['error'] = $e->getMessage();
+			$code = $e->getCode();
+			if(empty($code) || $code==0)  $code = 666;
+		}
+
+
+		$codes = array(
+			200 => 'HTTP/1.1 200 Ok',
+			201 => 'HTTP/1.1 201 Created',
+			204 => 'HTTP/1.1 204 No Content',
+			207 => 'HTTP/1.1 207 Multi-Status',
+			400 => 'HTTP/1.0 400 Bad Request',
+			401 => 'HTTP/1.0 401 Unauthorized',
+			403 => 'HTTP/1.1 403 Forbidden',
+			404 => 'HTTP/1.1 404 Not Found',
+			404 => 'HTTP/1.1 405 Method Not Allowed',
+			406 => 'HTTP/1.1 406 Not acceptable',
+			409 => 'HTTP/1.1 409 Conflict',
+			415 => 'HTTP/1.1 415 Unsupported Media Type',
+			423 => 'HTTP/1.1 423 Locked',
+			500 => 'HTTP/1.1 500 Internal Server Error',
+			501 => 'HTTP/1.1 501 Not implemented',
+			666 => 'HTTP/1.1 666 Welcome to Hell mate!',
+		);
+
+		if (ob_get_length()) ob_end_clean();
+		header(isset($codes[$code]) ? $codes[$code] : $codes[666]);
+		
+		foreach ($headers as $header) {
+			header($header);
+		}
+		
+		if(isset($_['pretty'])){
+			echo json_encode($response,JSON_PRETTY_PRINT);
+		}else{
+			echo json_encode($response);
+		}
+		exit();
+	}
+
+}
+
+
+class ApiRoute{
+	public $slug,$description,$callback,$method,$pathes,$parameters,$pattern;
+	function __construct($slug='default',$description='No description',$method='GET',$callback){
+		$this->pattern = $slug;
+
+		$infos = explode('?',$slug);
+		if(isset($infos[1])) $this->parameters = $infos[1];
+		$infos = explode('/',$infos[0]);
+
+		$this->slug = array_shift($infos);
+		$this->pathes = $infos;
+		$this->description = $description;
+		$this->method = $method;
+		$this->callback = $callback;
+	}
+}
+
+
+
+?>

+ 11 - 4
class/Configuration.class.php

@@ -56,11 +56,14 @@ class Configuration extends Entity
     public static function html($name){
         global $conf;
         $options = $GLOBALS['setting'][$name]; ?>
+
         <table id="<?php echo $name; ?>-setting-form" class="table table-striped">
             <tbody>
                 <?php foreach($options as $key=>$infos): ?>
-                    <?php if(!is_array($infos)): ?>
-                    <tr><th colspan="2"><h4><i class="fas fa-angle-right"></i> <?php echo $infos;?></h4></th></tr>
+                    <?php
+                        $input = '';
+                     if(!is_array($infos)): ?>
+                    <tr><th colspan="2"><h4 class="m-0"><i class="fas fa-angle-right"></i> <?php echo $infos;?></h4></th></tr>
                     <?php continue; endif; ?>
                     <tr>
                         <th class="align-middle"><?php echo $infos['label']; ?>
@@ -74,7 +77,7 @@ class Configuration extends Entity
                             if(isset($infos['placeholder'])) $attributes[] = 'placeholder="'.$infos['placeholder'].'"';
                             $attributes['class'] = 'class="form-control"';
                             $attributes[] = 'id="'.$key.'"';
-                            $attributes['value'] = 'value="'.$conf->get($key).'"';
+                            $attributes['value'] = 'value="'.htmlentities($conf->get($key)).'"';
 
                             if(isset($infos['parameters'])){
                                 foreach($infos['parameters'] as $attribute=>$parameter){
@@ -125,11 +128,15 @@ class Configuration extends Entity
                                  case 'date':
                                     $input = '<input type="text" data-type="date" '.implode(' ',$attributes).' >';
                                 break;
+                                case 'textarea':
+                                    $input = '<textarea '.implode(' ',$attributes).' >'.htmlentities($conf->get($key)).'</textarea>';
+                                break;
                                 case 'file':
                                     $input = '<input type="file" '.implode(' ',$attributes).' >';
                                 break;
                                 default:
-                                    $input = '<input type="text" '.implode(' ',$attributes).' >';
+                                    Plugin::callHook('configuration_fields',array(&$input, $infos, $attributes));
+                                    if(empty($input)) $input = '<input type="text" '.implode(' ',$attributes).' >';
                                 break;
                             }
                             echo $input; ?>

+ 44 - 2
class/Entity.class.php

@@ -238,6 +238,7 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
            ));
 
         $instance->customExecute($query);
+        if(isset($instance->indexes)) $instance->index($instance->indexes);
     }
 
     public static function drop()
@@ -251,6 +252,7 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
            ));
         
         $instance->customExecute($query);
+        if(isset($instance->indexes)) $instance->index($instance->indexes,false);
     }
 
     /**
@@ -697,6 +699,45 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
 
         return $instance->customExecute(Entity::render($sql,$data), $values);
     }
+
+    /**
+     * Méthode d'indexation de la ou les colonnes ciblées
+     * nb : il est possible d'appeller automatiquement cette méthode sur les classes entity lors du create
+     * si la classe contient l'attribut $this->indexes = array(...);
+     * @author Valentin CARRUESCO
+     *
+     * @category manipulation SQL
+     *
+     * @param <Array> | <String>  $colonne(s)  
+     * @param <Boolean>  Mode (true : ajout, false : suppression)
+     *
+     * @return Aucun retour
+     */
+    public static function index($columns,$mode = true){
+        if(!is_array($columns)) $columns = array($columns);
+        $columns = array_filter($columns);
+
+        $sgbd = BASE_SGBD;
+        
+        $class = get_called_class();
+        foreach($columns as $column){
+            if(!is_array($column)) $column = array($column);
+            $data = array(
+                'table' => $class::tableName(),
+                'column' =>  '`'.implode('`,`',$column).'`',
+                'index_name' =>  $class::tableName().'_'.implode('_',$column),
+            );
+
+            $results = $class::staticQuery(Entity::render($sgbd::count_index(),$data));
+            $exists = $results->fetch();
+            
+            if($mode){
+                if($exists['exists'] != 1) $class::staticQuery(Entity::render($sgbd::create_index(),$data));
+            }else{
+                if($exists['exists'] > 0) $class::staticQuery(Entity::render($sgbd::drop_index(),$data));
+            }
+        }
+    }
      
     public static function paginate($itemPerPage,$currentPage,&$query,$data){
         global $_;
@@ -707,7 +748,7 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
 
         $queryNumber = $query;
 
-        $queryNumber = preg_replace("/(SELECT.+[\n|\t]*)FROM[\s\t\r\n]/iU", 'SELECT '.$obj->tableName().'.'.$key.' FROM ',$queryNumber);
+        $queryNumber = preg_replace("/(SELECT.+[\n|\t]*)FROM[\s\t\r\n]/iU", 'SELECT DISTINCT '.$obj->tableName().'.'.$key.' FROM ',$queryNumber);
 
         $queryNumber = $class::staticQuery('SELECT COUNT(*) FROM ('.$queryNumber.') number',$data)->fetch();
 
@@ -719,7 +760,8 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         $query .= $limit;
         return array(
             'pages' => $pageNumber,
-            'current' => $currentPage
+            'current' => $currentPage,
+            'total' => $number
         );
     }
 

+ 13 - 2
class/Excel.class.php

@@ -11,7 +11,18 @@ class Excel
 		return SimpleXLSX::parse($xlsxFile);
 	}
 	
-	//Retourne le contenu d'un tableau convertit au format XLSX
+	/** 
+	 * Retourne le contenu d'un tableau convertit au format XLSX
+	 * @param  array $rows      le tableau avec les lignes à exporter dans le fichier
+	 * @param  array $mapping   le mapping qui permet d'ajouter des libellé en fct des clés de colonne
+	 * Structure du tableau de mapping attendu : aaray(
+	 * 		'Label' => 'clé',
+	 *   	'Autre label' => 'autre clé',
+	 *    	etc...
+	 * );
+	 * @param  string $sheetname le nom à donner à la feuille de calcul courante
+	 * @return string            le stream du fichier généré
+	 */
 	public static function exportArray($rows, $mapping, $sheetname='Classeur 1'){ 
 		$data = $header = array();
 		$style = array(
@@ -19,7 +30,7 @@ class Excel
 			'border-style' => 'thin'
 		);
 
-		if(!isset($mapping) && isset($rows[0])){
+		if(empty($mapping) && isset($rows[0])){
 			foreach($rows[0] as $key=>$value){
 				$mapping[$key] = $key;
 			}

+ 31 - 13
class/File.class.php

@@ -3,7 +3,8 @@
 class File{
 	
 	//Gère l'upload d'un fichier des vérifications au stockage dans le dossier file
-	public static function upload($index,$path,$maxSize = 1048576,$extensions = array('jpg','png','jpeg','gif','bmp') ){
+	public static function upload($index,$path,$maxSize = 1048576,$extensions = array('jpg','png','jpeg','gif','bmp') ,$trimParentDir = true){
+		if($trimParentDir) $path = str_replace('..','',$path);
 		if(!isset($_FILES[$index])) throw new Exception("L'index fichier '".$index."' n'existe pas");
 		if($_FILES[$index]['error'] != 0) throw new Exception("Erreur lors de l'envois du fichier : N° ".$_FILES[$index]['error']);
 		$extension = getExt($_FILES[$index]['name']);
@@ -27,8 +28,11 @@ class File{
 		);
 	}
 
-	public static function move($file,$path){
-		
+	public static function move($file,$path,$trimParentDir = true){
+		if($trimParentDir){
+			$file = str_replace('..','',$file);
+			$path = str_replace('..','',$path);
+		}
 		$extension = getExt($path);
 		$filePath = str_replace(array('/','\\','{{ext}}'),array(SLASH,SLASH,$extension),$path);
 		$fileName = basename($filePath);
@@ -58,8 +62,8 @@ class File{
 	}
 
 
-	public static function delete($namespace,$file){
-
+	public static function delete($namespace,$file,$trimParentDir = true){
+		if($trimParentDir) $file = str_replace('..','',$file);
 		$file =  $namespace.SLASH.substr($file, strlen($namespace)+1);
 		$file =  File::dir().$file;
 		$file =  str_replace(array('\\','/'),array(SLASH,SLASH),$file);
@@ -68,8 +72,8 @@ class File{
 		unlink($file);
 	}
 
-	public static function downloadFile($path,$name=null,$mime = null,$forceDownload = null){
-
+	public static function downloadFile($path,$name=null,$mime = null,$forceDownload = null,$trimParentDir = true){
+		if($trimParentDir) $path = str_replace('..','',$path);
 		if(!file_exists($path)) throw new Exception("Fichier inexistant :".$path);
 		$stream = file_get_contents($path);
 		if(!isset($mime)){
@@ -124,15 +128,19 @@ class File{
 		}
 	}
 
-	public static function copy($source,$destination){
+	public static function copy($source,$destination,$trimParentDir = true){
+		if($trimParentDir) $source = str_replace('..','',$source);
+		if($trimParentDir) $destination = str_replace('..','',$destination);
+		
 		if(is_file($source)) return copy($source,$destination);
 		$dir = opendir($source); 
-	    mkdir($destination,0755,true); 
-	    while(false !== ($file = readdir($dir))) { 
-	        if (($file != '.') && ($file != '..')) { 
-	            if (is_dir($source . SLASH . $file)) { 
+	    if(!file_exists($destination)) mkdir($destination,0755,true); 
+	    while(false !== ( $file = readdir($dir)) ) { 
+	        if (( $file != '.' ) && ( $file != '..' )) { 
+	            if ( is_dir($source . SLASH . $file) ) { 
 	                self::copy($source.SLASH.$file,$destination.SLASH.$file); 
-	            } else { 
+	            } 
+	            else { 
 	                copy($source.SLASH.$file,$destination.SLASH.$file); 
 	            } 
 	        } 
@@ -140,6 +148,16 @@ class File{
 	    closedir($dir); 
 	}
 
+	
+	public static function convert_encoding($path){
+		return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? utf8_encode($path) : $path;
+	}
+	
+	public static function convert_decoding($path){
+		return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? utf8_decode($path) : $path;
+	}
+
+
 }
 
 ?>

+ 3 - 3
class/Image.class.php

@@ -31,7 +31,7 @@ class Image{
 		$white = imagecolorallocate($output,  255, 255, 255);
 		imagefilledrectangle($output, 0, 0, $width, $height, $white);
 		imagecopy($output, $input, 0, 0, 0, 0, $width, $height);
-	    // compression is a value from 0 (worst) to 9 (best)
+	    // compression is a value from 0 (no compression) to 9 (high compression)
 		imagepng($output, $infos['dirname'].SLASH.$infos['filename'].'.png', 9);
 
 		unlink($path);
@@ -76,7 +76,7 @@ class Image{
 	    }
 	    if($infos['extension']=='jpg' || $infos['extension']=='jpeg') {
 	    	imagecopyresampled($dst_resource,$resource,0,0,0,0,$thumb_w,$thumb_h,$oldX,$oldY); 
-	        $result = imagejpeg($dst_resource,$path,100);
+	        $result = imagejpeg($dst_resource,$path,80);
 	    }
 	    imagedestroy($dst_resource); 
     	imagedestroy($resource);
@@ -84,7 +84,7 @@ class Image{
 
 	public static function resource($path){
 		$infos = pathinfo($path);
-		switch ($infos['extension']){
+		switch (mb_strtolower($infos['extension'])){
 			case 'jpg':
 			case 'jpeg':
 				return imagecreatefromjpeg($path);

+ 38 - 1
class/Log.class.php

@@ -33,7 +33,8 @@ class Log extends Entity
         $log->save();
     }
 
-    //Compare deux instances d'une même entité et log les differences entr eles deux dans un format json
+    //Compare deux instances d'une même entité et log
+    //les differences entre les deux dans un format JSON
     public static function compare($old,$new = null,$metafunction=null){
         global $myUser;
 
@@ -71,4 +72,40 @@ class Log extends Entity
        $treshold = time() - ($delay * 86400);
        self::delete(array('created:<'=>$treshold));
     }
+
+    
+    //Fonction permettant un benchmark simple et précis.
+    public static function benchmark($title = ""){
+        global $lastBench;
+        $start = microtime(true);
+        $benfile = __DIR__.SLASH.'..'.SLASH.'benchmark.debug.log';
+        $bt =  debug_backtrace();
+
+        $newBench = array(
+                'time' =>  microtime(true),
+                'file' =>  str_replace(__ROOT__,'',$bt[0]['file']),
+                'line' =>  $bt[0]['line'],
+         );
+
+        $label = '';
+        if(!isset($lastBench)){
+            $label .= PHP_EOL.PHP_EOL.'['.($title=="" ? "BENCHMARK" : $title ).'] - Début requete > benchmark : '. number_format(microtime(true)-$_SERVER["REQUEST_TIME_FLOAT"],4).' ms'.PHP_EOL;
+            $label .= PHP_EOL."\t".'=> '.$newBench['file'].PHP_EOL.PHP_EOL;
+           
+        }else{
+            if($lastBench['file']!= $newBench['file'])  $label .= PHP_EOL."\t".'=> '.$newBench['file'].PHP_EOL;
+            $time = $newBench['time'] - $lastBench['time'];
+            $title.= ' L.'.$lastBench['line'].' > L.'.$newBench['line'].' '.$title;
+            if($time>0.500) $title.=' <!> LENTEUR !!';
+            $label .= "\t\t".number_format($time,4).' ms | '.$title.PHP_EOL;
+        }
+
+        file_put_contents($benfile, $label,FILE_APPEND);
+        //on enleve au benchmark le temps d'execution de la fonciton de benchmark elle meme
+        $newBench['time'] = $newBench['time'] - ( microtime(true) - $start);
+        $lastBench = $newBench;
+    }
+
+    
+    
 }

+ 1 - 1
class/Mail.class.php

@@ -96,7 +96,7 @@ class Mail{
 			set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) { 
 				throw new Exception("Erreur d'envois de mail n°".$errno." :".$errstr);
 			});
-			mail(implode(',',$this->recipients['to']), $title, $stream, $headers);
+			mail(implode(',',array_unique($this->recipients['to'])), $title, $stream, $headers);
 			restore_error_handler();
 		}catch(Exception $e){
 			return $e->getMessage();

+ 32 - 33
class/Pdf.class.php

@@ -33,20 +33,19 @@
 class Pdf{
 	public $html,$orientation,$margin,$css;
 
-	function __construct($html = null){
-		$this->orientation = 'Portrait';
+	function __construct($html = null, $margin = array(), $format = 'A4', $orientation = 'Portrait'){
 		$this->html = $html;
-		$this->format = 'A4';
 		$this->margin = (object) array(
-			'top' => '10',
-			'right' => '10',
-			'bottom' => '10',
-			'left' => '10'
+			'top' => isset($margin['top']) ? $margin['top'] : '10',
+			'right' => isset($margin['right']) ? $margin['right'] : '10',
+			'bottom' => isset($margin['bottom']) ? $margin['bottom'] : '10',
+			'left' => isset($margin['left']) ? $margin['left'] : '10'
 		);
+		$this->format = $format;
+		$this->orientation = $orientation;
 	}
 
-
-		// Retourne (uniquement sur les pdf avec texte editable) la liste des champs editable
+	// Retourne (uniquement sur les pdf avec texte editable) la liste des champs editable
 	// du pdf spécifié et leurs valeurs. Si le parametre onlyfdf est a true, la méthode retournera
 	// le format fdf brut (utile uniquement pour du débug ou pour de l'interne)
 	// NB : Penser a installer ``apt-get install pdftk`` (ou pdftk pour windows) sur le système avant de lancer ce script
@@ -101,30 +100,30 @@ class Pdf{
 		$foot = '';
 
 		//Récupération du head html
-        if(preg_match("/<!DOCTYPE.*<body.*>/isU", $body, $match))
-        	$head = $match[0];
+	    if(preg_match("/<!DOCTYPE.*<body.*>/isU", $body, $match))
+	    	$head = $match[0];
 
-        //Récupération du footer html
-        if(preg_match("/<\/body>.*<\/html>/isU", $body, $match))
-        	$foot = $match[0];
+	    //Récupération du footer html
+	    if(preg_match("/<\/body>.*<\/html>/isU", $body, $match))
+	    	$foot = $match[0];
 
 		//Récupération du header pdf
-        if(preg_match("/<!--[\s\t\r\n]*#header[\s\t\r\n]*-->(.*)<!--[\s\t\r\n]*\/header[\s\t\r\n]*-->/isU", $body, $match))
-        	$header = $match[0];
-
-        //Récupération du footer pdf 
-        if(preg_match("/<!--[\s\t\r\n]*#footer[\s\t\r\n]*-->(.*)<!--[\s\t\r\n]*\/footer[\s\t\r\n]*-->/isU", $body, $match))
-        	$footer = $match[0];
-
-        //Récupération du body
-        if(isset($header)){
-        	$body = str_replace($header, '', $body);
-        	$header = $head.$header.$foot;
-        }
-        if(isset($footer)){
-        	$body = str_replace($footer, '', $body);
-        	$footer = $head.$footer.$foot;
-        }
+	    if(preg_match("/<!--[\s\t\r\n]*#header[\s\t\r\n]*-->(.*)<!--[\s\t\r\n]*\/header[\s\t\r\n]*-->/isU", $body, $match))
+	    	$header = $match[0];
+
+	    //Récupération du footer pdf 
+	    if(preg_match("/<!--[\s\t\r\n]*#footer[\s\t\r\n]*-->(.*)<!--[\s\t\r\n]*\/footer[\s\t\r\n]*-->/isU", $body, $match))
+	    	$footer = $match[0];
+
+	    //Récupération du body
+	    if(isset($header)){
+	    	$body = str_replace($header, '', $body);
+	    	$header = $head.$header.$foot;
+	    }
+	    if(isset($footer)){
+	    	$body = str_replace($footer, '', $body);
+	    	$footer = $head.$footer.$foot;
+	    }
 
 		file_put_contents($bodyPath, $body);
 		$outcmd = array();
@@ -132,17 +131,17 @@ class Pdf{
 		$cmd = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? '"C:'.SLASH.'Program Files'.SLASH.'wkhtmltopdf'.SLASH.'bin'.SLASH.'wkhtmltopdf.exe" ' : "/usr/local/bin/wkhtmltopdf.sh ";
 		$cmd .= '-s '.$this->format.' -O '.$this->orientation.' -T '.$this->margin->top.' -R '.$this->margin->right.' -B '.$this->margin->bottom.' -L '.$this->margin->left;
 		$cmd .= ' -d 100 --print-media-type ';
-		$cmd .= ' --user-style-sheet '.dirname(__FILE__).'/../'.$this->css.' --enable-javascript ';
+		$cmd .= ' --user-style-sheet '.dirname(__FILE__).'/../'.$this->css.' --enable-javascript --javascript-delay 1000 ';
 		
 		if(isset($header)) {
 			$headerPath = File::dir().'tmp'.SLASH.'header-'.$fileName.'.html';
 			file_put_contents($headerPath, $header);
-			$cmd .= ' --header-html '.$headerPath.' --header-spacing 5 --margin-top 35';
+			$cmd .= ' --header-html '.$headerPath.' --header-spacing 5 --margin-top 40';
 		}
 		if(isset($footer)) {
 			$footerPath = File::dir().'tmp'.SLASH.'footer-'.$fileName.'.html';
 			file_put_contents($footerPath, $footer);
-			$cmd .= ' --footer-html '.$footerPath.' --footer-spacing 5 --margin-bottom 35';
+			$cmd .= ' --footer-html '.$footerPath.' --footer-spacing 5 --margin-bottom 30';
 		}
 		$cmd .= ' --footer-right "[page] / [toPage]" --footer-font-size 8 ';
 		$cmd .= $bodyPath.' '.$pdfPath;

+ 4 - 4
class/Plugin.class.php

@@ -141,14 +141,14 @@ class Plugin{
 	}
 
 	public static function url(){
-		$bt =  debug_backtrace();
-		return ROOT_URL.SLASH.PLUGIN_PATH.basename(dirname($bt[0]['file']));
+		$bt = debug_backtrace();
+		return ROOT_URL.'/'.str_replace(SLASH, '/', PLUGIN_PATH).basename(dirname($bt[0]['file']));
 	}
 
 
 	public static function addCss($css) { 
 		$bt =  debug_backtrace();
-		if(substr($css, 0,4) != 'http') $css = str_replace(__ROOT__,'',dirname($bt[0]['file'])).$css;
+		if(substr($css, 0,4) != 'http') $css = str_replace(array(__ROOT__,'\\'),array('','/'),dirname($bt[0]['file'])).$css;
 		$GLOBALS['hooks']['css_files'][] = $css;  
 	}
 
@@ -166,7 +166,7 @@ class Plugin{
 	public static function addJs($js){  
 		global $_;
 		$bt =  debug_backtrace();
-		if(substr($js, 0,4) != 'http') $js = str_replace(__ROOT__,'',dirname($bt[0]['file'])).$js;
+		if(substr($js, 0,4) != 'http') $js = str_replace(array(__ROOT__,'\\'),array('','/'),dirname($bt[0]['file'])).$js;
 		$GLOBALS['hooks']['js_files'][] = $js;  
 	}
 

+ 1 - 1
class/Right.class.php

@@ -12,7 +12,7 @@ class Right extends Entity
     protected $fields =
     array(
         'id' => 'key',
-        'rank' => 'string',
+        'rank' => 'int',
         'section' => 'string',
         'firm' => 'int',
         'read' => 'boolean',

+ 71 - 68
class/User.class.php

@@ -27,6 +27,7 @@ class User extends Entity
         'phone' => 'string',
         'mobile' => 'string',
         'manager' => 'string',
+        'origin' => 'string',
         'service' => 'string',
         'superadmin' => 'int'
     );
@@ -37,44 +38,47 @@ class User extends Entity
         $this->meta = array();
     }
 
-    public static function getAll($loadRight = true,$force = false){
+    public static function getAll($loadRight=true, $force=false){
         $users = array();
         if(isset($_SESSION['users']))
             $users = unserialize($_SESSION['users']);
 
         if(empty($users) || $force){
             Plugin::callHook('user_load',array(&$users, $loadRight));
-            $_SESSION['users'] = serialize($users);
-        }
+            uasort($users, function($a, $b){ return strcmp($a->name, $b->name); });
 
-        foreach (self::loadAll() as $baseUser) {
-            $existingKey = null;
-
-            if($loadRight){
-                if(!isset($baseUser->ranks)) $baseUser->ranks = array();
-                if(!isset($baseUser->firms)) $baseUser->firms = array();
-                $baseUser->loadRanks();
-                $baseUser->loadRights();
-            }
+            foreach (self::loadAll(array('state'=>User::ACTIVE), array(' name ASC ')) as $baseUser) {
+                $existingKey = null;
 
-            foreach ($users as $key => $otherUser) {
-                //permet la predominance des users db sur les user foreign type type
-                if($otherUser->login == $baseUser->login){
-                    $existingKey  = $key;
-                    break;
+                if($loadRight){
+                    if(!isset($baseUser->ranks)) $baseUser->ranks = array();
+                    if(!isset($baseUser->firms)) $baseUser->firms = array();
+                    $baseUser->loadRanks();
+                    $baseUser->loadRights();
                 }
+                if(isset($baseUser->manager) && $baseUser->manager!="") $baseUser->manager = User::load(array('login'=>$baseUser->manager));
+
+                foreach ($users as $key => $otherUser) {
+                    //permet la predominance des users db sur les user foreign type type
+                    if($otherUser->login == $baseUser->login){
+                        $existingKey = $key;
+                        break;
+                    }
+                }
+                if(isset($existingKey)){
+                    $users[$existingKey] = $baseUser;
+                }else{
+                    $users[] = $baseUser;
+                } 
             }
-            if(isset($existingKey)){
-                $users[$existingKey] = $baseUser;
-            }else{
-                $users[] = $baseUser;
-            } 
+            $_SESSION['users'] = serialize($users);
         }
+
         return $users;
     }
     
     public function __sleep(){
-        return array_merge(array('rights','ranks','firms','preferences','origin','meta'),array_keys($this->toArray()));
+        return array_merge(array('rights','ranks','firms','preferences','meta'),array_keys($this->toArray()));
     }
 
     public function can($section,$right){
@@ -91,6 +95,24 @@ class User extends Entity
         return false;
     }
 
+    //Lance les exception appropriées en fonction du droit ou des droits spécifiés
+    // ex : User::checkAccess('document','configure');
+    public static function check_access($section,$right){
+        global $myUser;
+        if(!isset($myUser) || !is_object($myUser) || !$myUser->connected()) throw new Exception("Contrôle d'accès - Vous devez être connecté",401);
+        if(!$myUser->can($section,$right)) throw new Exception("Contrôle d'accès - Permissions insuffisantes",403);
+    }
+
+    public function hasRank($rankId){
+        if($this->superadmin) return true;
+        $rankIds = array();
+        global $myFirm;
+        if(empty($this->ranks) || !isset($this->ranks[$myFirm->id])) return false;
+        foreach ($this->ranks[$myFirm->id] as $rank)
+            $rankIds[$rank->id] = true;
+        return isset($rankIds[$rankId]);
+    }
+
     public function preference($key=null, $value=null){   
         if(!isset($key) && !isset($value)) return $this->preferences;
         if(isset($key) && !isset($value)) return isset($this->preferences[$key])?$this->preferences[$key]:'';
@@ -112,7 +134,7 @@ class User extends Entity
             $firm = $firmRank->join('firm');
             $this->firms[$firm->id] = $firm;
             if(!isset($this->ranks[$firmRank->firm])) $this->ranks[$firmRank->firm] = array();
-            $this->ranks[$firmRank->firm][]= $rank;
+            $this->ranks[$firmRank->firm][$rank->id]= $rank;
         }
     }
 
@@ -170,25 +192,6 @@ class User extends Entity
         return in_array($id, array_keys($this->firms));
     }
 
-        //Lance les exception appropriées en fonction du droit ou des droits spécifiés
-    // ex : User::checkAccess('document','configure');
-    public static function check_access($section,$right){
-        global $myUser;
-        if(!isset($myUser) || !is_object($myUser) || !$myUser->connected()) throw new Exception("Contrôle d'accès - Vous devez être connecté",401);
-        if(!$myUser->can($section,$right)) throw new Exception("Contrôle d'accès - Permissions insuffisantes",403);
-    }
-
-    public function hasRank($rankId){
-        if($this->superadmin) return true;
-        $rankIds = array();
-        global $myFirm;
-        if(empty($this->ranks) || !isset($this->ranks[$myFirm->id])) return false;
-        foreach ($this->ranks[$myFirm->id] as $rank)
-            $rankIds[$rank->id] = true;
-        return isset($rankIds[$rankId]);
-    }
-
-
     public function getAvatar($getPath = false){
         $avatar = 'img/default-avatar.png';
         $files = glob(__ROOT__.FILE_PATH.AVATAR_PATH.$this->login.'.{jpg,png,jpeg,gif}',GLOB_BRACE);
@@ -203,11 +206,15 @@ class User extends Entity
     public static function check($login, $password, $loadRight = true) {   
         global $myFirm;
         $user = self::load(array('login' => $login, 'password' => self::password_encrypt($password)));
-       
+        
+        //load from plugins
+        Plugin::callHook("user_login", array(&$user,$login,$password,$loadRight));
+
         //load from db
         if($user!=false){
-            $user->ranks = array();
-            $user->firms = array();
+            $user->ranks = empty($user->ranks) ? array() : $user->ranks;
+            $user->firms = empty($user->firms) ? array() : $user->firms;
+            if(isset($user->manager) && !empty($user->manager) && !is_object($user->manager)) $user->manager = self::byLogin($user->manager);
             $user->loadRanks();
             $user->loadPreferences();
 
@@ -217,23 +224,21 @@ class User extends Entity
                 $user->setFirms($firms);
             }
 
-            $defaultFirm = !empty($user->preference('default_firm')) ? $user->preferences['default_firm'] : key($user->firms);
-            $myFirm = isset($user->firms[$defaultFirm]) ? $user->firms[$defaultFirm]:reset($user->firms);
-            if(!isset($user->firms[$defaultFirm])) $user->preference('default_firm',$myFirm->id);
-
+            if(!empty($user->firms)){
+                $defaultFirm = !empty($user->preference('default_firm')) ? $user->preferences['default_firm'] : key($user->firms);
+                $myFirm = isset($user->firms[$defaultFirm]) ? $user->firms[$defaultFirm]:reset($user->firms);
+                if(!isset($user->firms[$defaultFirm])) $user->preference('default_firm',$myFirm->id);
+            }
             if($loadRight) $user->loadRights();
         }
-        //load from plugings
-        Plugin::callHook("user_login", array(&$user,$login,$password,$loadRight));
 
         $user = is_object($user) ? $user : new self();
         return $user;
     }
 
-    public static function byLogin($login,$loadRight = true){
-        foreach(User::getAll($loadRight) as $user){
-            if($user->login == $login)  return $user;
-        }
+    public static function byLogin($login,$loadRight=true){
+        foreach(User::getAll($loadRight) as $user)
+            if($user->login == $login) return $user;
         return new User();
     }
 
@@ -257,27 +262,27 @@ class User extends Entity
      public function subordinates(){
         $subordinates = array();
         foreach (User::getAll() as $user) {
-            if(is_object($user->manager) && $user->manager->login == $myUser->login) $subordinates[] = $user;
+            if(is_object($user->manager) && $user->manager->login == $this->login) $subordinates[] = $user;
         }
         return $subordinates;
     }
 
-    public static function passwordFormats(){
+    public static function password_formats(){
         $formats = array(
-            array('pattern'=>'|[0-9]|i','label'=>'Le mot de passe doit comporter au minimum 1 chiffre (norme anssi)'),
-            array('pattern'=>'|[A-Z]|','label'=>'Le mot de passe doit comporter au minimum 1 majuscule (norme anssi)'),
-            array('pattern'=>'|[^A-Za-z0-9éèêëàäâïîöôûüù]|i','label'=>'Le mot de passe doit comporter au minimum 1 caractère spécial (norme anssi)'),
+            array('pattern'=>'|[0-9]|i','label'=>'Le mot de passe doit comporter au minimum 1 chiffre (norme ANSSI)'),
+            array('pattern'=>'|[A-Z]|','label'=>'Le mot de passe doit comporter au minimum 1 majuscule (norme ANSSI)'),
+            array('pattern'=>'|[^A-Za-z0-9éèêëàäâïîöôûüù]|i','label'=>'Le mot de passe doit comporter au minimum 1 caractère spécial (norme ANSSI)'),
             array('pattern'=>'|.{6,}|','label'=>'Le mot de passe doit comporter au minimum 6 caractères'),
-            array('pattern'=>'|.{12,}|','label'=>'Le mot de passe doit comporter au minimum 12 caractères (norme anssi)'),
+            array('pattern'=>'|.{12,}|','label'=>'Le mot de passe doit comporter au minimum 12 caractères (norme ANSSI)'),
         );
         return $formats;
     }
 
-    public static function checkPasswordFormat($password){
+    public static function check_password_format($password){
         global $conf;
         $errors = array();
         $formats = array();
-        foreach (self::passwordFormats() as $format) {
+        foreach (self::password_formats() as $format) {
             $formats[$format['pattern']] = $format;
         } 
         $selectedFormats = json_decode($conf->get('password_format'),true);
@@ -294,13 +299,11 @@ class User extends Entity
 
 
 
-    public static function password_encrypt($password)
-    {
+    public static function password_encrypt($password){
         return sha1(md5($password));
     }
 
-    public function connected()
-    {
+    public function connected(){
         return !empty($this->login);
     }
 

+ 25 - 21
common.php

@@ -28,31 +28,37 @@ $conf->getAll();
 
 //CONFS GÉNÉRALES
 Configuration::setting('configuration-global',array(
-    "Gestion des pages :",
+    "Général :",
     'home_page' => array("label"=>"Page d'accueil","type"=>"text","legend"=>"Laisser vide pour gérer en automatique","placeholder"=>"eg : index.php?module=example"),
+    'show_application_name' => array("label"=>"Afficher le nom du programme", "legend"=>"Dans la barre de navigation / menu uniquement", "type"=>"checkbox"),
     "Gestion des clés Map Algolia API :",
     'maps_api_id' => array("label"=>"ID de l'application","type"=>"text","legend"=>"Clé API pour le composant location","placeholder"=>"eg. pl0749TULNDW..."),
     'maps_api_key' => array("label"=>"Clé publique de l'application","type"=>"password","legend"=>"Clé API pour le composant location","placeholder"=>"eg. db6788b1e4165d3370ed88a304704676..."),
     "Authentification :",
     'account_block' => array("label"=>"Activer le blocage de compte au bout de N essais","legend"=>"Tous les utilisateurs seront soumis à la règle","type"=>"checkbox"),
     'account_block_try' => array("label"=>"Nombre d'essais avant blocage du compte","legend"=>"L'utilisateur aura N tentatives pour se connecter avant d'être bloqué","type"=>"number", "placeholder"=>"eg. 10"),
-    'account_block_delay' => array("label"=>"Durée de blocage", "legend"=>"(en minutes)", "type"=>"number", "placeholder"=>"eg. 30")
+    'account_block_delay' => array("label"=>"Durée de blocage", "legend"=>"(en minutes)", "type"=>"number", "placeholder"=>"eg. 30"),
+    'Mots de passe <div class="btn btn-warning btn-small float-right" onclick="general_reset_password_delay()"><i class="fas fa-exclamation-triangle"></i> Forcer le renouvellement</div>',
+    'password_delay'=>array("label"=>"Renouvellement", "legend"=>"Forcer l'utilisateur a renouveller son mot de passe tous les X jours (laisser vide pour désactiver)", "type"=>"number", "placeholder"=>"eg. 30"),
+    'password_allow_lost'=>array("label"=>"Oubli de mot de passe", "legend"=>"Proposer la récuperation du mot de passe oublié", "type"=>"checkbox"),
+    "Connectivité :",
+    'offline_mode' => array("label"=>"Activer le mode hors ligne","legend"=>"(Désactive toutes les fonctionnalités ayant besoin d'un accès internet depuis le poste client cdn...)","type"=>"checkbox"),
 ));
 
 //CACHE CSS & JS
 $cacheVersion = 1;
-if(file_exists(__DIR__.SLASH.'.git'.SLASH.'refs'.SLASH.'heads'.SLASH.'master'))
-	$cacheVersion = file_get_contents(__DIR__.SLASH.'.git'.SLASH.'refs'.SLASH.'heads'.SLASH.'master');
-
-
+if(file_exists(__DIR__.SLASH.'.git'.SLASH.'HEAD')){
+	$versionFile = str_replace(array('ref: ',PHP_EOL,"\r","\n"),'',file_get_contents(__DIR__.SLASH.'.git'.SLASH.'HEAD'));
+	if(file_exists(__DIR__.SLASH.'.git'.SLASH.$versionFile)){
+		$cacheVersion = str_replace(array("\r","\n"),'',file_get_contents(__DIR__.SLASH.'.git'.SLASH.$versionFile));
+	}
+}
 
 if($myUser->login==null && isset($_COOKIE[COOKIE_NAME])){
-
 	$cookie = UserPreference::load(array('key'=>'cookie','value'=>$_COOKIE[COOKIE_NAME]));
 
 	if($cookie!=false){
-
-	    if(Plugin::is_active('fr.idleman.activedirectory'))
+	    if(Plugin::is_active('fr.sys1.activedirectory'))
 	        require_once(PLUGIN_PATH.'activedirectory'.SLASH.'activedirectory.plugin.php');
 	    
 	    $myUser = User::byLogin($cookie->user);
@@ -64,13 +70,10 @@ if($myUser->login==null && isset($_COOKIE[COOKIE_NAME])){
         }
         
         $defaultFirm = !empty($myUser->preference('default_firm')) ? $myUser->preferences['default_firm'] : key($myUser->firms);
-        
         $myFirm = isset($myUser->firms[$defaultFirm]) ? $myUser->firms[$defaultFirm]:key($myUser->firms);
         
         $_SESSION['currentUser'] = serialize($myUser);
         $_SESSION['firm'] = serialize($myFirm);
-	        
-	    
     }
 }
 
@@ -167,7 +170,7 @@ Plugin::addHook("menu_setting", function(&$settingMenu){
 			'label' => 'Logs',
 			'category' => 'administration'
 		);
-
+	
 	if($myUser->login!='')
 		$settingMenu[]= array(
 			'sort' =>16,
@@ -180,7 +183,6 @@ Plugin::addHook("menu_setting", function(&$settingMenu){
 
 Plugin::addHook("menu_main", function(&$mainMenu) {
 	global $myUser;
-
 	
 	$mainMenu[] = array(
 		'sort' =>0,
@@ -229,7 +231,7 @@ Plugin::addHook("menu_user", function(&$userMenu){
 		
 		$userMenu[]= array(
 			'sort' =>1,
-			'custom' => "<div class='firm-item' onclick='event.stopPropagation();'><small>Établissement : </small><select class=\"form-control\" onchange=\"window.location='action.php?action=select_firm&firm='+$(this).val();\">".$options."</select></div><div class='dropdown-divider'></div>",
+			'custom' => "<div class='firm-item' onclick='event.stopPropagation();'><small>Établissement : </small><select class=\"form-control form-control-sm\" onchange=\"window.location='action.php?action=select_firm&firm='+$(this).val();\">".$options."</select></div><div class='dropdown-divider'></div>",
 		);
 
 	} else {
@@ -240,12 +242,13 @@ Plugin::addHook("menu_user", function(&$userMenu){
 		);
 	}
 
-	$userMenu[]= array(
-		'sort' =>0,
-		'label' => 'Mon compte',
-		'icon' => $userIcon,
-		'url' => 'account.php'
-	);
+	if($myUser->can('account','read'))
+		$userMenu[]= array(
+			'sort' =>0,
+			'label' => 'Mon compte',
+			'icon' => $userIcon,
+			'url' => 'account.php'
+		);
 
 	if($myUser->can('setting_global', 'read'))
 		$userMenu[]= array(
@@ -285,6 +288,7 @@ Plugin::addHook("section",function(&$sections){
 	$sections['log'] = 'Gestion des logs programme';
 	$sections['dictionnary'] = 'Gestion des listes programme';
 	$sections['file'] = 'Gestion des fichiers';
+	$sections['account'] = 'Gestion du compte courant';
 });
 
 Plugin::addHook("cron",function(){

+ 16 - 0
connector/Mysql.class.php

@@ -73,6 +73,22 @@ class Mysql
 		$sql = 'TRUNCATE TABLE `{{table}}`;';
 		return $sql;
 	}
+
+	public static function create_index(){
+		$sql = 'CREATE INDEX `{{index_name}}` ON `{{table}}` ({{column}})';
+		return $sql;
+	}
+
+	public static function drop_index(){
+		$sql = 'DROP INDEX `{{index_name}}` ON `{{table}}`';
+		return $sql;
+	}
+
+	public static function count_index(){
+		$sql = "SELECT COUNT(1) `exists` FROM INFORMATION_SCHEMA.STATISTICS
+		WHERE table_schema=DATABASE() AND table_name='{{table}}' AND index_name='{{index_name}}'";
+		return $sql;
+	}
 }
 
 ?>

+ 18 - 0
connector/Sqlite.class.php

@@ -68,6 +68,24 @@ class Sqlite
 		$sql = 'TRUNCATE TABLE "{{table}}";';
 		return $sql;
 	}
+
+	public static function create_index(){
+		$sql = 'CREATE INDEX IF NOT EXISTS `{{index_name}}` ON `{{table}}` (`{{column}}`)';
+		return $sql;
+	}
+
+	public static function drop_index(){
+		$sql = 'DROP INDEX `{{index_name}}` ON `{{table}}`';
+		return $sql;
+	}
+
+	public static function count_index(){
+		//On desactive le check sur sqlite, la notion IF NOT EXISTS existant dans ce sgbd
+		$sql = "SELECT 0 `exists`";
+		return $sql;
+	}
+
+
 }
 
 ?>

+ 9 - 5
constant-sample.php

@@ -23,11 +23,15 @@ define('BASE_DEBUG',false);
 
 define('COOKIE_NAME','erp-core-cookie');
 
-define('PROGRAM_NAME','Hackpoint');
-define('PROGRAM_UID','idleman/hackpoint');
-define('PROGRAM_TECHNICIAN','valentin.carruesco');
+define('PROGRAM_NAME','Sys1 ERP');
+define('PROGRAM_UID','sys1/erp-core');
+define('PROGRAM_TECHNICIAN','valentin.morreel');
+//Windows
+define('REFERENCE_URL','https://projet.sys1.fr/action.php?action=reference_save_project');
+//Linux
+// define('REFERENCE_URL','http://projet.sys1.fr/action.php?action=reference_save_project');
 
-define('SOURCE_VERSION','1.5');
-define('BASE_VERSION','1.5');
+define('SOURCE_VERSION','1.0');
+define('BASE_VERSION','1.0');
 
 ?>

File diff suppressed because it is too large
+ 3 - 3
css/bootstrap.min.css


File diff suppressed because it is too large
+ 0 - 0
css/bootstrap.min.css.map


+ 208 - 51
css/main.css

@@ -1,3 +1,35 @@
+/* latin-ext */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 300;
+  src: local('Lato Light'), local('Lato-Light'), url(../fonts/lato-light-latin-ext.woff2) format('woff2');
+  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 300;
+  src: local('Lato Light'), local('Lato-Light'), url(../fonts/lato-light-latin.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Lato Regular'), local('Lato-Regular'), url(../fonts/lato-regular-latin-ext.woff2) format('woff2');
+  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Lato';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Lato Regular'), local('Lato-Regular'), url(../fonts/lato-regular-latin.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
 /* Page de maintenance */
 .maintenance-page,
 .maintenance-page body {
@@ -46,6 +78,53 @@
     color:#e55039;
 }
 
+/** PRELOADER **/
+.loader {
+	position: relative;
+	top: calc(50% - 32px);
+	left: calc(50% - 32px);
+	width: 64px;
+	height: 64px;
+	border-radius: 50%;
+	perspective: 800px;
+}
+.inner {
+	position: absolute;
+	box-sizing: border-box;
+	width: 100%;
+	height: 100%;
+	border-radius: 50%;  
+}
+.inner.one {
+	left: 0%;
+	top: 0%;
+	animation: rotate-one 1s linear infinite;
+	border-bottom: 4px solid #0070B3;
+}
+.inner.two {
+	right: 0%;
+	top: 0%;
+	animation: rotate-two 1s linear infinite;
+	border-right: 4px solid #00AEEA;
+}
+.inner.three {
+	right: 0%;
+	bottom: 0%;
+	animation: rotate-three 1s linear infinite;
+	border-top: 4px solid #B5B5B5;
+}
+@keyframes rotate-one {
+	0% { transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg); }
+	100% { transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg); }
+}
+@keyframes rotate-two {
+	0% { transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg); }
+	100% { transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg); }
+}
+@keyframes rotate-three {
+	0% { transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg); }
+	100% { transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg); }
+}
 
 /* SETTINGS */
 .maintenance-block textarea {
@@ -79,22 +158,24 @@
 	width: 100%;
 }
 
-/* Général */
-
-
+/* Popover bootstrap */
+.popover-body * {
+	width: 100%;
+}
 
 /* fix ie10 et ie11 mr-auto et div parent en justify-content */
 .modal-footer-margin-auto{
 	justify-content: space-around !important;
 }
 
+/* Général */
 html {
 	position: relative;
 	min-height: 100%;
 }
 body {
 	/* Margin bottom by footer height */
-	margin-bottom: 60px;
+	margin-bottom: 50px;
 	font-family: 'Lato', sans-serif;
 }
 
@@ -102,12 +183,18 @@ body > .container {
 	padding: 60px 15px 0;
 }
 
+div[required="required"],
 input:required,
 textarea:required,
 select:required {
 	border-left: 0.25rem solid #FFA100 !important;
 }
 
+div[required="required"]{
+	border-top-left-radius: 4px;
+	border-bottom-left-radius: 4px;
+}
+
 #mainMenu{
 	background : #343a40;
 	padding: 5px;
@@ -410,6 +497,9 @@ div.advanced-search div.filter-value-block select.form-control.filter-value {
 	max-width: 160px;
 	min-width: 218px;
 }
+div.filter-box div.simple-search > div.input-group-prepend {
+	z-index: 10;
+}
 .filter-box .advanced-search .filter-value {
 	max-width: 160px;
 }
@@ -418,13 +508,13 @@ div.advanced-search div.filter-value-block select.form-control.filter-value {
 	margin: 0 5px;
 }
 .filter-box .advanced-search{
-	border:1px solid #eaeaea;
-	padding-bottom:10px;
+	border: 1px solid #eaeaea;
+	padding-bottom: 10px;
+	margin-top: 10px;
 }
 #search-clear {
 	position: absolute;
 	opacity: 0;
-	top: 9px;
 	bottom: 0;
 	height: 25px;
 	margin: auto;
@@ -711,7 +801,7 @@ li.active .dropdown-item{
 	font-size: 15px;
 }
 .btn-squarred.btn-small {
-	padding: 3px 7px;
+	padding: 3px 5px;
 	font-size: 1em;
 	width: 31px;
 	height: 31px;
@@ -724,7 +814,7 @@ li.active .dropdown-item{
     border-radius: .2rem;
 }
 .btn-squarred.btn-mini {
-	padding: 0 3px;
+	padding: 0;
 	font-size: 0.8em;
 	width: 21px;
 	height: 21px;
@@ -758,9 +848,6 @@ li.active .dropdown-item{
 .noLabel{
 	margin-top: 31px;
 }
-.noDisplay{
-	display:none;
-}
 .no-select {
 	user-select: none !important;
 }
@@ -783,6 +870,30 @@ li.active .dropdown-item{
 .pagination {
 	flex-wrap: wrap;
 }
+.pagination [data-toggle="dropdown"]{
+	position:absolute;
+	top:0;
+	left:0;
+	width:100%;
+	height:100%;
+	text-align:center;
+	line-height: 1.70;
+}
+.pagination [data-toggle="dropdown"] > i{
+	transform: scale(0.7);
+	line-height: 2.5;
+	opacity:0.5;
+	transition: all 0.2s ease-in-out;
+}
+.pagination [data-toggle="dropdown"]:hover > i{
+	opacity: 1;
+	transform: scale(0.9);
+}
+.pagination .pagination-select {
+	height: auto;
+	max-height: 200px;
+	overflow-x: hidden;
+}
 .no-select {
 	user-select: none !important;
 }
@@ -815,6 +926,9 @@ input[data-type="user"]{
 	display:none!important;
 }
 
+
+
+/* composant user */
 div.data-type-user{
 	background-image: url('../img/icons/icon-user.png');
 	background-repeat:no-repeat;
@@ -822,11 +936,13 @@ div.data-type-user{
 	padding-left: 30px !important;
 	height:inherit !important;
 }
-
-
 div.data-type-user[required] {
 	border-left: 0.25rem solid #FFA100 !important;
 }
+div.data-type-user[disabled],
+div.data-type-user[readonly] {
+	background-color:#e9ecef !important;
+}
 
 div.data-type-user > ul > li > input{
 	border:0;
@@ -836,6 +952,7 @@ div.data-type-user > ul > li > input{
 	padding:0;
 }
 div.data-type-user > ul{
+	cursor: text;
 	border:0;
 	margin:0;
 	padding:0;
@@ -856,14 +973,10 @@ div.data-type-user > ul > li{
 div.data-type-user > ul > li > div{
 	display: flex;
 	font-size: 12px;
-	
 	border-bottom: 1px solid #4caf50;
 	text-overflow: ellipsis;
-
 	white-space: nowrap;
-    border-radius: 3px;
-    padding: 0 5px 0 5px;
-    background-color: #e9ecef;
+    padding: 0px;
 }
 div.data-type-user > ul > li > div > i{
 	margin: auto;
@@ -874,7 +987,7 @@ div.data-type-user > ul > li > div > i{
 }
 div.data-type-user ul.dropdown-menu .dropdown-item {
 	position: relative;
-    padding: .25rem 1.25rem;
+    padding: .25rem 0.50rem;
 }
 div.data-type-user  ul.dropdown-menu div.user-logo,
 div.data-type-user   ul.dropdown-menu div.rank-logo {
@@ -1121,10 +1234,13 @@ input[type="color"].form-control{
 	position: unset;
 }
 .component-icon div.dropdown-menu {
-	max-height: 400px;
 	width: 100%;
 	max-width: 550px;
-	overflow-x: auto;
+}
+.component-icon div.dropdown-menu > div.icons-list {
+	max-height: 300px;
+	overflow-y: auto;
+	padding-left: 5px;
 }
 .component-icon .dropdown-item {
 	display: block;
@@ -1133,11 +1249,11 @@ input[type="color"].form-control{
 	width: 100%;
 }
 .component-icon .dropdown-icon-item {
-    cursor: pointer;
-    display: inline-block;
-    width: 35px;
-    height: 35px;
-    padding:10px;
+	cursor: pointer;
+	display: inline-flex;
+	width: 35px;
+	height: 35px;
+	transition: all 0.08s linear;
 }
 .component-icon .dropdown-icon-item:focus,
 .component-icon .dropdown-icon-item:active,
@@ -1147,7 +1263,7 @@ input[type="color"].form-control{
     background-color: #007bff;
     border-radius: 3px;
 }
-.component-icon .dropdown-item > span > i {
+.component-icon span.dropdown-icon-item > i {
     margin: auto;
 }
 .icon-chooser button {
@@ -1164,6 +1280,21 @@ input[type="color"].form-control{
 	color: unset;
 	background-color: unset;
 }
+
+.component-icon .no-icon{
+	border: 2px dashed #cecece;
+	color:#cecece;
+	border-radius: 5px;
+	padding:5px;
+	transition: all 0.2s ease-in-out;
+}
+.component-icon  .no-icon.dropdown-icon-item:focus, .component-icon  .no-icon.dropdown-icon-item:active, .component-icon  .no-icon.dropdown-icon-item:hover {
+    border-color: #ffc107;
+    color: #ffc107;
+	background-color: transparent;
+	border-radius: 5px;
+}
+
 /* FIN COMPOSANT ICÔNES */
 
 /* BACK TO TOP */
@@ -1192,17 +1323,10 @@ input[type="color"].form-control{
 /* FIN BACK TO TOP */
 
 /* COMPOSANT FILTER */
-.filter-box{
-	display: none;
-}
 .input-group>.input-group-append>.btn.filter-button-search,
 .input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle).filter-button-search{
 	border-radius: 0 3px 3px 0;
 }
-.filter-box .advanced-search{
-	display: none;
-	margin-top: 10px;
-}
 .filterRow{
 	margin: 3px 0;
 }
@@ -1256,24 +1380,30 @@ div#ui-datepicker-div{
 	z-index:10000 !important;
 }
 
+/* COMPOSANT TAG LIST */
+.typeahead.dropdown-menu{
+	overflow: auto;
+	max-height: 300px;
+}
+
 /* CUSTOM CHECKBOXES */
-.check-component {
+label.check-component {
     width: 15px;
     height: 15px;
     margin: 0 5px;
     /*margin: auto;*/
     border: 1px solid #bbbbbb;
 }
-.check-component input {
+label.check-component input {
     display: none;
 }
-.check-component input:checked + .box {
+label.check-component input:checked + .box {
     background-color: #ddf8d8;
 }
-.check-component input:indeterminate + .box {
+label.check-component input:indeterminate + .box {
     background-color: #ffd6c9;
 }
-.check-component .box {
+label.check-component .box {
     width: 100%;
     height: 100%;
     transition: .35s cubic-bezier(0.19, 1, 0.22, 1);
@@ -1285,7 +1415,7 @@ div#ui-datepicker-div{
     cursor: pointer;
     box-shadow: 0 1px rgba(0, 0, 0, 0.2);
 }
-.check-component input:checked + .box:after {
+label.check-component input:checked + .box:after {
     width: 85%;
     height: 45%;
     border-left: 1.5px solid;
@@ -1295,13 +1425,13 @@ div#ui-datepicker-div{
     transform-origin: center center;
     top: 0;
 }
-.check-component input:indeterminate + .box:after {
+label.check-component input:indeterminate + .box:after {
 	width: 75%;
     height: 0;
 	top: 0;
 	border-top: 1.5px solid rgba(50, 50, 50,.5);;
 }
-.check-component .box:after {
+label.check-component .box:after {
     content: '';
     position: absolute;
     transition: .35s cubic-bezier(0.19, 1, 0.22, 1);
@@ -1312,6 +1442,14 @@ div#ui-datepicker-div{
     bottom: 5%;
     margin: auto;
 }
+label[disabled].check-component input:checked + .box,
+label[disabled].check-component .box {
+    background-color: #e8e8e8;
+    cursor: not-allowed;
+}
+label[disabled].check-component input:checked + .box:after {
+    border-color: #565656;
+}
 
 /* CUSTOM RADIO */
 .radio-component:checked,
@@ -1368,15 +1506,28 @@ div#ui-datepicker-div{
     -webkit-transform: scale(1);
     transform: scale(1);
 }
+.radio-component:checked + label[disabled],
+.radio-component:not(:checked) + label[disabled] {
+    cursor: not-allowed;
+}
+.radio-component:checked + label[disabled]:before,
+.radio-component:not(:checked) + label[disabled]:before {
+	border: 1px solid #2541274a;
+	box-shadow: 0 1px rgba(0, 0, 0, 0.2);
+	background: #e8e8e8;
+}
 
+/* CARD COMPONENT */
 *[data-type="card"].card-component-container {
 	cursor: pointer;
 	color: #2b9ad8;
-	position: static;
+	position: relative;
 }
 *[data-type="card"].card-component-container > div.wrapper {
+	position: absolute;
 	display: block;
 	min-height: 0px;
+	z-index: 10;
 }
 *[data-type="card"].card-component-container:hover {
 	text-decoration: underline;
@@ -1547,11 +1698,16 @@ div#ui-datepicker-div{
 #logs .log-col {
 	width: 700px;
 	max-width: 700px;
+	word-break: break-all;
 }
 
+.avatar-large{
+	width: 140px;
+	height: 140px;
+}
 .avatar{
-	width: 120px;
-	height: 120px;
+	width: 100px;
+	height: 100px;
 }
 .avatar-medium{
 	width: 60px;
@@ -1566,6 +1722,7 @@ div#ui-datepicker-div{
 }
 .avatar-login {
 	margin-right: 5px;
+	object-fit: cover;
 }
 .default {
 	cursor: default;
@@ -1610,8 +1767,8 @@ div#ui-datepicker-div{
 	bottom: 0;
 	width: 100%;
 	/* Set the fixed height of the footer here */
-	height: 60px;
-	line-height: 60px; /* Vertically center the text there */
+	height: 50px;
+	line-height: 50px; /* Vertically center the text there */
 	background-color: #f5f5f5;
 }
 
@@ -1651,11 +1808,11 @@ code {
 	text-align: left;
 }
 
-.hidden,.hide,
-.form-control.hidden,
-.form-control.hide{
+.hidden,
+.form-control.hidden{
 	display: none!important;
 }
+
 .invisible,
 .form-control.invisible{
 	visibility: hidden;

+ 1 - 3
firm.php

@@ -1,7 +1,5 @@
 <?php require_once __DIR__.DIRECTORY_SEPARATOR.'header.php';  
-
-if (!$myUser->connected()) header('location: index.php');
-if(!$myUser->can('firm','edit')) throw new Exception("Permissions insuffisantes",403);
+User::check_access('firm','edit');
 $firm = Firm::provide();
 
 ?>

BIN
fonts/lato-light-latin-ext.woff2


BIN
fonts/lato-light-latin.woff2


BIN
fonts/lato-regular-latin-ext.woff2


BIN
fonts/lato-regular-latin.woff2


+ 163 - 150
footer.php

@@ -8,175 +8,188 @@ $mediaRoot = isset($mediaRoot) ? $mediaRoot : define_media_root();
 $loadingTime = isset($start_time) ? (round(microtime(TRUE) - $start_time,5)) : '';
 $cacheVersion = isset($cacheVersion) ? $cacheVersion : 1;
 ?>
-<footer class="footer noPrint">
-	<div class="container">
-		<span class="text-muted"><?php echo PROGRAM_NAME.' V'.SOURCE_VERSION.'b'.BASE_VERSION; ?>  by <a href="<?php echo $scheme; ?>://idleman.fr" target="_blank">@IdleCorp</a> <!--| <a href="file/guide/manuel-utilisateur.pdf" target="_blank"><i class="far fa-file-pdf"></i> Documentation</a> -->-  <?php echo $loadingTime; ?></span>
-	</div>
-	<div id="toTheTop" title="Retour en haut de page"></div>
-</footer>
-
-<!-- Composant filtre -->
-<div class="filter-box">
-	<div class="input-group simple-search">
-		<div class="input-group-prepend">
-			<div class="input-group-text">Recherche</div>
+	<footer class="footer noPrint">
+		<div class="container">
+			<span class="text-muted"><?php echo PROGRAM_NAME.' V'.SOURCE_VERSION.'b'.BASE_VERSION; ?>  by <a href="<?php echo $scheme; ?>://sys1.fr" target="_blank">@Sys1</a> <!--| <a href="file/guide/manuel-utilisateur.pdf" target="_blank"><i class="far fa-file-pdf"></i> Documentation</a> -->-  <?php echo $loadingTime; ?></span>
 		</div>
-		<input type="text" class="form-control filter-keyword" placeholder="Mot clé">
-		<span id="search-clear" class="fas fa-times"></span>
-		<div class="input-group-append">
-			<div class="btn btn-info filter-button-search" title="Rechercher" onclick="filter_search($(this).closest('.filter-box'));"><i class="fas fa-search"></i> Rechercher</div>
-			<a class="btn btn-link text-muted pointer advanced-button-search" onclick="switch_advanced_filter(this);"><i class="fas fa-filter"></i> Filtrer</a>
+		<div id="toTheTop" title="Retour en haut de page"></div>
+	</footer>
+
+	<!-- Composant filtre -->
+	<div class="filter-box hidden">
+		<div class="input-group simple-search">
+			<div class="input-group-prepend">
+				<div class="input-group-text">Recherche</div>
+			</div>
+			<input type="text" class="form-control filter-keyword" placeholder="Mot clé">
+			<span id="search-clear" class="fas fa-times"></span>
+			<div class="input-group-append">
+				<div class="btn btn-info filter-button-search" title="Rechercher" onclick="filter_search($(this).closest('.filter-box'));"><i class="fas fa-search"></i> Rechercher</div>
+				<a class="btn btn-link text-muted pointer advanced-button-search" onclick="switch_advanced_filter(this);"><i class="fas fa-filter"></i> Filtrer</a>
+			</div>
 		</div>
-	</div>
-	<div class="advanced-search">
-		<h6 class="p-3 d-inline-block font-weight-bold text-uppercase">Recherche avancée</h6>
-		<small class="text-muted mb-2 p-3 d-inline-block right">
-			<a class="pointer hidden advanced-search-save" onclick="filter_save(this)" title="Enregistrer la recherche"><i class="far fa-hdd"></i> Enregistrer</a>
-			<span class="advanced-search-action-separator hidden"> | </span>
-			<a class="pointer" onclick="filter_clean(this)" title="Nettoyer la recherche"><i class="fas fa-broom"></i> Nettoyer</a>
-		</small>
-		<div class="row filterRow form-inline">
-			<div class="col-xl-12 p-0">
-				<select class="form-control filter-join">
-					<option value="and">Et</option>
-					<option value="or">Ou</option>
-				</select>
+		<div class="advanced-search hidden">
+			<h6 class="p-3 d-inline-block font-weight-bold text-uppercase">Recherche avancée</h6>
+			<small class="text-muted mb-2 p-3 d-inline-block right">
+				<a class="pointer hidden advanced-search-save" onclick="filter_save(this)" title="Enregistrer la recherche"><i class="far fa-hdd"></i> Enregistrer</a>
+				<span class="advanced-search-action-separator hidden"> | </span>
+				<a class="pointer" onclick="filter_clean(this)" title="Nettoyer la recherche"><i class="fas fa-broom"></i> Nettoyer</a>
+			</small>
+			<div class="row filterRow form-inline">
+				<div class="col-xl-12 p-0">
+					<select class="form-control filter-join ml-3">
+						<option value="and">Et</option>
+						<option value="or">Ou</option>
+					</select>
 
-				<select class="form-control filter-column font-weight-bold border-0" onchange="filter_set_column(this)"></select>
+					<select class="form-control filter-column font-weight-bold border-0" onchange="filter_set_column(this)"></select>
 
-				<div readonly="readonly" class="form-control filter-values">Sélectionnez un filtre</div>
-				<div class="btn pr-1" title="Supprimer ce filtre" onclick="filter_delete(this)"><i class="far fa-trash-alt text-danger"></i></div>
-				<div class="btn pl-0" title="Ajouter un filtre" onclick="filter_add(this)" style=""><i class="fas fa-plus text-success"></i></div>
-			</div>
+					<div readonly="readonly" class="form-control filter-values">Sélectionnez un filtre</div>
+					<div class="btn pr-1 filter-delete-button" title="Supprimer ce filtre" onclick="filter_delete(this)"><i class="far fa-trash-alt text-danger"></i></div>
+					<div class="btn pl-0 filter-add-button" title="Ajouter un filtre" onclick="filter_add(this)" style=""><i class="fas fa-plus text-success"></i></div>
+				</div>
 
-			<div class="filter-value-block" data-value-type="text">
-				<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
-					<option value="=">Egal</option>
-					<option value="LIKE">Contient</option>
-					<option value="!=">Différent</option>
-					<option value="IS NULL"  data-values="0">Non Renseigné</option>
-					<option value="IS NOT NULL" data-values="0">Renseigné</option>
-				</select> <input type="text" placeholder="Mot clé" data-custom="{{filterCustom}}" class="form-control filter-value">
-			</div>
-			<div class="filter-value-block" data-value-type="user" >
-				<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
-					<option value="=">Egal</option>
-					<option value="!=">Différent</option>
-					<option value="IS NULL"  data-values="0">Non Renseigné</option>
-					<option value="IS NOT NULL" data-values="0">Renseigné</option>
-				</select> <input type="text" data-template="user" data-custom="{{filterCustom}}" data-force="false" placeholder="Utilisateur" class="form-control filter-value">
-			</div>
-			<div class="filter-value-block" data-value-type="number" >
-				<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
-					<option value="=">Egal</option>
-					<option value="between" data-values="2">Entre</option>
-					<option value=">">Supérieur à</option>
-					<option value="<">Inférieur à</option>
-					<option value="IS NULL"  data-values="0">Non Renseigné</option>
-					<option value="IS NOT NULL" data-values="0">Renseigné</option>
-				</select> <input type="number" data-template="number" data-custom="{{filterCustom}}" placeholder="Nombre" class="form-control filter-value">
-			</div>
-			<div class="filter-value-block" data-value-type="date" >
-				<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
-					<option value="<">Avant</option>
-					<option value=">">Après</option>
-					<option value="between" data-values="2">Entre</option>
-					<option value="=">Le</option>
-					<option value="IS NULL"  data-values="0">Non Renseigné</option>
-					<option value="IS NOT NULL" data-values="0">Renseigné</option>
-				</select>
-				<input type="text" placeholder="dd/mm/yyyy" data-template="date" data-custom="{{filterCustom}}" class="form-control filter-value">
-			</div>
-			<div class="filter-value-block" data-value-type="dictionnary" >
-				<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
-					<option value="=">Egal</option>
-					<option value="!=">Différent</option>
-					<option value="IS NULL"  data-values="0">Non Renseigné</option>
-					<option value="IS NOT NULL" data-values="0">Renseigné</option>
-				</select>
-				<select data-template="dictionnary" data-custom="{{filterCustom}}" data-slug="{{slug}}" data-depth="{{depth}}" data-filter-type-value="{{filterTypeValue}}" class="form-control filter-value" data-disable-label></select>
+				<div class="filter-value-block" data-value-type="text">
+					<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
+						<option value="=">Égal</option>
+						<option value="LIKE">Contient</option>
+						<option value="NOT LIKE">Ne contient pas</option>
+						<option value="!=">Différent</option>
+						<option value="IS NULL"  data-values="0">Non Renseigné</option>
+						<option value="IS NOT NULL" data-values="0">Renseigné</option>
+					</select> <input type="text" placeholder="Mot clé" data-custom="{{filterCustom}}" class="form-control filter-value">
+				</div>
+				<div class="filter-value-block" data-value-type="user" >
+					<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
+						<option value="=">Égal</option>
+						<option value="!=">Différent</option>
+						<option value="IS NULL"  data-values="0">Non Renseigné</option>
+						<option value="IS NOT NULL" data-values="0">Renseigné</option>
+					</select> <input type="text" data-template="user" data-custom="{{filterCustom}}" data-force="false" placeholder="Utilisateur" class="form-control filter-value">
+				</div>
+				<div class="filter-value-block" data-value-type="number" >
+					<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
+						<option value="=">Égal</option>
+						<option value="between" data-values="2">Entre</option>
+						<option value=">">Supérieur à</option>
+						<option value="<">Inférieur à</option>
+						<option value="IS NULL"  data-values="0">Non Renseigné</option>
+						<option value="IS NOT NULL" data-values="0">Renseigné</option>
+					</select> <input type="number" data-template="number" data-custom="{{filterCustom}}" placeholder="Nombre" class="form-control filter-value">
+				</div>
+				<div class="filter-value-block" data-value-type="date" >
+					<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
+						<option value="<">Avant</option>
+						<option value=">">Après</option>
+						<option value="between" data-values="2">Entre</option>
+						<option value="=">Le</option>
+						<option value="IS NULL"  data-values="0">Non Renseigné</option>
+						<option value="IS NOT NULL" data-values="0">Renseigné</option>
+					</select>
+					<input type="text" placeholder="dd/mm/yyyy" data-template="date" data-custom="{{filterCustom}}" class="form-control filter-value">
+				</div>
+				<div class="filter-value-block" data-value-type="dictionnary" >
+					<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
+						<option value="=">Égal</option>
+						<option value="!=">Différent</option>
+						<option value="IS NULL"  data-values="0">Non Renseigné</option>
+						<option value="IS NOT NULL" data-values="0">Renseigné</option>
+					</select>
+					<select data-template="dictionnary" data-custom="{{filterCustom}}" data-slug="{{slug}}" data-depth="{{depth}}" data-filter-type-value="{{filterTypeValue}}" class="form-control filter-value" data-disable-label></select>
+				</div>
+				<div class="filter-value-block" data-value-type="boolean" >
+					<select class="form-control filter-operator border-0 text-primary" onchange="filter_set_comparator(this)">
+						<option value="=">Est coché</option>
+						<option value="!=">N'est pas coché</option>
+						<option value="IS NULL"  data-values="0">Non Renseigné</option>
+						<option value="IS NOT NULL" data-values="0">Renseigné</option>
+					</select>
+					<input type="text" data-template="boolean" class="form-control filter-value hidden" value="1">
+				</div>
 			</div>
+			<div class="btn btn-info mt-1 mr-2 right" title="Rechercher" onclick="filter_search($(this).closest('.filter-box'));"><i class="fas fa-search"></i> Rechercher</div>
+			<div class="clear"></div>
 		</div>
-		<div class="btn btn-info mt-4 mr-2 right" title="Rechercher" onclick="filter_search($(this).closest('.filter-box'));"><i class="fas fa-search"></i> Rechercher</div>
-		<div class="clear"></div>
 	</div>
-</div>
 
-<!-- Composant icone -->
-<div class="dropdown component-icon hide">
-  <button class="btn btn-primary dropdown-toggle" type="button"  data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-   	<i class="{{value}}"></i>
-  </button>
-  <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
-  	<a class="dropdown-item" onclick="event.stopPropagation();" href="#">
-    	<input type="text" placeholder="Mot clé" class="form-control w-100">
-    </a>
-    {{#choices}}
-    	
+	<!-- Composant icone -->
+	<div class="dropdown component-icon hidden">
+	  <button class="btn btn-primary dropdown-toggle" type="button"  data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+	   	<i class="{{value}}"></i>
+	  </button>
+	  <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+	  	<a class="dropdown-item" onclick="event.stopPropagation();" href="#">
+	    	<input type="text" placeholder="Mot clé" class="form-control w-100">
+	    </a>
+	    <div class="dropdown-divider"></div>
+	    <div class="icons-list">
+	    	<span class="dropdown-icon-item no-icon" title="Pas d'icône" data-icon="hidden"><i class="far fa-eye-slash"></i></span>
+	    {{#choices}}
     		{{#.}}<span class="dropdown-icon-item" title="{{.}}" data-icon="{{.}}"><i class="{{.}}"></i></span>{{/.}}
-    	
-    {{/choices}}
-    
-  </div>
-</div>
+	    {{/choices}}
+	    </div>
+	  </div>
+	</div>
 
-<!-- Composant quickform -->
-<div class="modal fade quickform-modal" id="quickform-modal" tabindex="-1" role="dialog" aria-labelledby="quickform-modal-label" aria-hidden="true">
-	<div class="modal-dialog modal-lg" role="document">
-		<div class="modal-content">
-			<div class="modal-header">
-				<h5 class="modal-title" id="quickform-modal-label"></h5>
-				<button type="button" class="close" data-dismiss="modal" aria-label="Fermer">
-					<span aria-hidden="true">&times;</span>
-				</button>
-			</div>
-			<div class="modal-body">
-			</div>
-			<div class="modal-footer">
-				<div class="btn btn-light" data-dismiss="modal">Fermer</div>
+	<!-- Composant quickform -->
+	<div class="modal fade quickform-modal" id="quickform-modal" tabindex="-1" role="dialog" aria-labelledby="quickform-modal-label" aria-hidden="true">
+		<div class="modal-dialog modal-lg" role="document">
+			<div class="modal-content">
+				<div class="modal-header">
+					<h5 class="modal-title" id="quickform-modal-label"></h5>
+					<button type="button" class="close" data-dismiss="modal" aria-label="Fermer">
+						<span aria-hidden="true">&times;</span>
+					</button>
+				</div>
+				<div class="modal-body"></div>
+				<div class="modal-footer">
+					<div class="btn btn-light" data-dismiss="modal">Fermer</div>
+				</div>
 			</div>
 		</div>
 	</div>
-</div>
 
-<!-- Composant dictionnary table -->
-<div class="hide component-dictionnary-table" data-id="" data-type="dictionnary-table"  data-dictionnary="{{parent.id}}">
-    <table id="" class="table table-striped table-bordered table-hover">
-        <thead>
-            <tr>
-                <th colspan="<?php echo $myUser->superadmin ? 3 : 2; ?>">{{parent.label}}</th>
-            </tr>
-            <tr class="edit-line">
-                <th><input class="form-control edit-field list-label" type="text" placeholder="Libellé de la liste"></th>
-                <?php if($myUser->superadmin): ?>
-                <th><input class="form-control edit-field list-slug" type="text" placeholder="Slug de la liste"></th>
-                <?php endif; ?>
-                <th class="action-cell text-center"><div class="btn btn-success" title="Ajouter un élément de liste"><i class="fas fa-check"></i></div></th>
-            </tr>
-        </thead>
-        <tbody>
-            <tr data-id="{{id}}" class="hidden">
-                <td>{{label}}</td>
-                <?php if($myUser->superadmin): ?>
-                <td>{{slug}}</td>
-                <?php endif; ?>
-                <td class="action-cell text-center">
-                    <span class="btn btn-info btn-squarred btn-mini btn-edit" title="Éditer l'élément de liste"><i class="fas fa-pencil-alt"></i></span>
-                    <span class="btn btn-danger btn-squarred btn-mini" title="Supprimer l'élément de liste"><i class="fas fa-times"></i></span>
-                </td>
-            </tr>
-        </tbody>
-    </table>
-</div>
+	<!-- Composant dictionnary table -->
+	<div class="hidden component-dictionnary-table" data-id="" data-type="dictionnary-table"  data-dictionnary="{{parent.id}}">
+	    <table id="" class="table table-striped table-bordered table-hover">
+	        <thead>
+	            <tr>
+	                <th colspan="<?php echo $myUser->superadmin ? 3 : 2; ?>">{{parent.label}}</th>
+	            </tr>
+	            <tr class="edit-line">
+	                <th><input class="form-control edit-field list-label" type="text" placeholder="Libellé de la liste"></th>
+	                <?php if($myUser->superadmin): ?>
+	                <th><input class="form-control edit-field list-slug" type="text" placeholder="Slug de la liste"></th>
+	                <?php endif; ?>
+	                <th class="action-cell text-center"><div class="btn btn-success" title="Ajouter un élément de liste"><i class="fas fa-check"></i></div></th>
+	            </tr>
+	        </thead>
+	        <tbody>
+	            <tr data-id="{{id}}" class="hidden">
+	                <td>{{label}}</td>
+	                <?php if($myUser->superadmin): ?>
+	                <td>{{slug}}</td>
+	                <?php endif; ?>
+	                <td class="action-cell text-center">
+	                    <span class="btn btn-info btn-squarred btn-mini btn-edit" title="Éditer l'élément de liste"><i class="fas fa-pencil-alt"></i></span>
+	                    <span class="btn btn-danger btn-squarred btn-mini" title="Supprimer l'élément de liste"><i class="fas fa-times"></i></span>
+	                </td>
+	            </tr>
+	        </tbody>
+	    </table>
+	</div>
 
 	<?php echo Plugin::callHook('application_bottom'); ?>
 	<!-- Bootstrap core JavaScript -->
 	<!-- Placed at the end of the document so the pages load faster -->
-	<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
-	<script>window.jQuery || document.write('<script src="<?php echo $mediaRoot ?>/js/vendor/jquery.min.js"><\/script>');</script>
-
-	<script id="algolia" data-api="<?php echo $conf->get('maps_api_id'); ?>" data-key="<?php echo $conf->get('maps_api_key'); ?>" src="https://cdn.jsdelivr.net/npm/places.js@1.15.1"></script>
+	<?php if($conf->get('offline_mode')): ?>
+		<script src="<?php echo $mediaRoot ?>/js/vendor/jquery.min.js"></script>
+	<?php else: ?>
+		<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
+		<script>window.jQuery || document.write('<script src="<?php echo $mediaRoot ?>/js/vendor/jquery.min.js"><\/script>');</script>
 
+		<script id="algolia" data-api="<?php echo $conf->get('maps_api_id'); ?>" data-key="<?php echo $conf->get('maps_api_key'); ?>" src="https://cdn.jsdelivr.net/npm/places.js@1.15.1"></script>
+	<?php endif; ?>
 
 	<!-- For fontawesome 5 pseudo-elements -->
 	<script> window.FontAwesomeConfig = {searchPseudoElements: true}</script>
@@ -195,5 +208,5 @@ $cacheVersion = isset($cacheVersion) ? $cacheVersion : 1;
 	<script src="<?php echo $mediaRoot ?>/js/plugins.js?v=<?php echo $cacheVersion ?>"></script>
 	<script src="<?php echo $mediaRoot ?>/js/main.js?v=<?php echo $cacheVersion ?>"></script>
 	<?php echo Plugin::callJs($mediaRoot,$cacheVersion); ?>
-</body>
+	</body>
 </html>

+ 135 - 111
function.php

@@ -12,9 +12,26 @@ function errorToException( $errno, $errstr, $errfile, $errline, $errcontext) {
 }
 
 function unhandledException($ex){
+	global $myUser;
 	//Récuperation de la requete échouée si l'utilisateur n'est pas authentifié
-	if($ex->getCode()==401) $_SESSION['last_request'] = $_SERVER['REQUEST_URI'];
-	echo '<div id="message" class="alert alert-danger"><strong>Erreur : </strong><span>'.$ex->getMessage().'  -  <small style="opacity:0.5;">'.$ex->getFile().' L'.$ex->getLine().'</small></span></div>';
+	switch ($ex->getCode()) {
+	    case 401:
+	        $_SESSION['last_request'] = $_SERVER['REQUEST_URI'];
+	        if(isset($_SESSION['logout_redirect'])){
+	        	echo '<script type="text/javascript">window.location="'.$_SESSION['logout_redirect'].'";</script>';
+	        }
+	        echo '<div id="message" class="alert alert-info"><strong>Connexion requise : </strong><span>'.$ex->getMessage();
+	        echo '<br> Pour vous connecter, cliquez sur le menu "Connexion" en haut à droite de cet écran';
+	        echo '</span></div>';
+	    break;
+	    
+	    default:
+	        echo '<div id="message" class="alert alert-danger"><strong>Erreur : </strong><span>'.$ex->getMessage();
+	        if($myUser->superadmin) echo '  -  <small style="opacity:0.5;">'.$ex->getFile().' L'.$ex->getLine().'</small>';
+	        echo '</span></div>';
+	    break;
+	}
+	
 	require_once('footer.php');
 	exit();
 }
@@ -31,7 +48,8 @@ function ip(){
 
 //Check si l'url serveur est en HTTPS
 function is_url_securised(){
-    return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off';
+	//La verification de HTTP_X_FORWARDED_PROTO permet de gerer les reverse proxy qui font du https -> http
+    return (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO']=='https') || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off');
 }
 
 //Définit le schéma d'URL à utiliser
@@ -39,6 +57,12 @@ function define_url_scheme(){
     return is_url_securised() ? 'https' : 'http';
 }
 
+function encode_uri($uri){
+    return preg_replace_callback("{[^0-9a-z_.!~*'();,/?:@&=+$#-]}i", function ($m) {
+        return sprintf('%%%02X', ord($m[0]));
+    }, $uri);
+}
+
 //Définit la racine des médias en
 //fonction du schéma d'URL (http ou https)
 function define_media_root(){
@@ -60,7 +84,6 @@ function decrypt($data){
 function slugify($text,$allowChars = '') {
 	setlocale(LC_CTYPE, 'fr_FR.UTF-8');
 	try{
-
 		$encoding = mb_detect_encoding($text, mb_detect_order(), false);
 	    if($encoding == "UTF-8") $text = mb_convert_encoding($text, 'UTF-8', 'UTF-8'); 
 	   
@@ -78,15 +101,10 @@ function slugify($text,$allowChars = '') {
 if(!function_exists('glob_recursive')) {
 	// Does not support flag GLOB_BRACE        
 	function glob_recursive($pattern, $flags = 0,$forbiddenPathes = array(),$root = null){
-
 		$files = glob($pattern, $flags);
-
 		foreach (glob(dirname($pattern).SLASH.'*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
-			
 			if(isset($root))
 				if(in_array(str_replace($root,'',$dir), $forbiddenPathes)) continue;
-			
-
 			$files = array_merge($files, glob_recursive($dir.SLASH.basename($pattern), $flags,$forbiddenPathes,$root));
 		}
 		return $files;
@@ -425,8 +443,8 @@ function relative_time($date,$date2 = null, $relativeLimit = null, $detailled =
     $intervalle = $from->diff($to);
 
     if(isset($relativeLimit) && $intervalle->format('%d') > $relativeLimit){
-    	$limitFormat = $detailled ? 'd/m/Y H:i' : 'd/m/Y';
-    	return 'Le '.date($limitFormat, $date);
+    	$limitFormat = $detailled ? 'd/m/Y - H:i' : 'd/m/Y';
+    	return date($limitFormat, $date);
     }
 
     $prefixe = $from > $to ? 'Dans ' :'Il y a ' ;
@@ -512,9 +530,13 @@ function filter_secure_query($filters,$allowedColumns,&$query,&$data){
 	foreach ($filters as $i=>$filter) {
 		$filter['operator'] = html_entity_decode($filter['operator']);
 		if(!in_array($filter['column'], $allowedColumns)) return;
-		if(!in_array(strtolower($filter['operator']), array('<','>','=','!=','like','in','not in','between','is null','is not null'))) return;
+		if(!in_array(strtolower($filter['operator']), array('<','>','=','!=','like','not like','in','not in','between','is null','is not null'))) return;
 		if(!in_array(strtolower($filter['join']), array('and','or'))) return;
-		if(strtolower($filter['type']) == 'date'){
+
+		if(!preg_match("/.*\..*/i", $filter['column']))
+		    $filter['column'] = '`'.$filter['column'].'`';
+
+		if(strtolower($filter['type']) == 'date' && isset($filter['value'])){
 			$filter['column'] = 'UNIX_TIMESTAMP(STR_TO_DATE(DATE_FORMAT(FROM_UNIXTIME('.$filter['column'].'),"%d/%m/%Y"), "%d/%m/%Y"))';
 			if(is_array($filter['value'])){
 				foreach($filter['value'] as $i=>$value){
@@ -527,6 +549,7 @@ function filter_secure_query($filters,$allowedColumns,&$query,&$data){
 		
 		switch(strtolower($filter['operator'])){
 			case 'like':
+			case 'not like':
 				$query .= ' '.$filter['join'].' '.$filter['column'].' '.$filter['operator'].' ?';
 				$data[] =  '%'.$filter['value'].'%' ;
 			break;
@@ -565,92 +588,96 @@ function check_mail($mail){
 }
 
 function truncate($text, $length = 100, $options = array()) {
-	$default = array(
-		'ending' => '...', 'exact' => true, 'html' => false
-	);
-	$options = array_merge($default, $options);
-	extract($options);
+    $default = array(
+        'ending' => '...',
+        'exact' => true,
+        'html' => false,
+        'keepTags' => true,
+    );
+    $options = array_merge($default, $options);
+    extract($options);
+
+    if ($html) {
+        if (mb_strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
+            return $text;
+        }
+        $totalLength = mb_strlen(strip_tags($ending));
+        $openTags = array();
+        $truncate = '';
+
+        preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER);
+        foreach ($tags as $tag) {
+            if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/s', $tag[2])) {
+                if (preg_match('/<[\w]+[^>]*>/s', $tag[0])) {
+                    array_unshift($openTags, $tag[2]);
+                } else if (preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag)) {
+                    $pos = array_search($closeTag[1], $openTags);
+                    if ($pos !== false) {
+                        array_splice($openTags, $pos, 1);
+                    }
+                }
+            }
+            $truncate .= $tag[1];
+
+            $contentLength = mb_strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $tag[3]));
+            if ($contentLength + $totalLength > $length) {
+                $left = $length - $totalLength;
+                $entitiesLength = 0;
+                if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $tag[3], $entities, PREG_OFFSET_CAPTURE)) {
+                    foreach ($entities[0] as $entity) {
+                        if ($entity[1] + 1 - $entitiesLength <= $left) {
+                            $left--;
+                            $entitiesLength += mb_strlen($entity[0]);
+                        } else {
+                            break;
+                        }
+                    }
+                }
 
-	if ($html) {
-		if (mb_strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
-			return $text;
-		}
-		$totalLength = mb_strlen(strip_tags($ending));
-		$openTags = array();
-		$truncate = '';
-
-		preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER);
-		foreach ($tags as $tag) {
-			if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/s', $tag[2])) {
-				if (preg_match('/<[\w]+[^>]*>/s', $tag[0])) {
-					array_unshift($openTags, $tag[2]);
-				} else if (preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag)) {
-					$pos = array_search($closeTag[1], $openTags);
-					if ($pos !== false) {
-						array_splice($openTags, $pos, 1);
-					}
-				}
-			}
-			$truncate .= $tag[1];
-
-			$contentLength = mb_strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $tag[3]));
-			if ($contentLength + $totalLength > $length) {
-				$left = $length - $totalLength;
-				$entitiesLength = 0;
-				if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $tag[3], $entities, PREG_OFFSET_CAPTURE)) {
-					foreach ($entities[0] as $entity) {
-						if ($entity[1] + 1 - $entitiesLength <= $left) {
-							$left--;
-							$entitiesLength += mb_strlen($entity[0]);
-						} else {
-							break;
-						}
-					}
-				}
+                $truncate .= mb_substr($tag[3], 0 , $left + $entitiesLength);
+                break;
+            } else {
+                $truncate .= $tag[3];
+                $totalLength += $contentLength;
+            }
+            if ($totalLength >= $length) {
+                break;
+            }
+        }
+    } else {
+        if (mb_strlen($text) <= $length) {
+            return $text;
+        } else {
+            $truncate = mb_substr($text, 0, $length - mb_strlen($ending));
+        }
+    }
 
-				$truncate .= mb_substr($tag[3], 0 , $left + $entitiesLength);
-				break;
-			} else {
-				$truncate .= $tag[3];
-				$totalLength += $contentLength;
-			}
-			if ($totalLength >= $length) {
-				break;
-			}
-		}
-	} else {
-		if (mb_strlen($text) <= $length) {
-			return $text;
-		} else {
-			$truncate = mb_substr($text, 0, $length - mb_strlen($ending));
-		}
-	}
-	if (!$exact) {
-		$spacepos = mb_strrpos($truncate, ' ');
-		if (isset($spacepos)) {
-			if ($html) {
-				$bits = mb_substr($truncate, $spacepos);
-				preg_match_all('/<\/([a-z]+)>/', $bits, $droppedTags, PREG_SET_ORDER);
-				if (!empty($droppedTags)) {
-					foreach ($droppedTags as $closingTag) {
-						if (!in_array($closingTag[1], $openTags)) {
-							array_unshift($openTags, $closingTag[1]);
-						}
-					}
-				}
-			}
-			$truncate = mb_substr($truncate, 0, $spacepos);
-		}
-	}
-	$truncate .= $ending;
+    if (!$exact) {
+        $spacepos = mb_strrpos($truncate, ' ');
+        if (isset($spacepos)) {
+            if ($html) {
+                $bits = mb_substr($truncate, $spacepos);
+                preg_match_all('/<\/([a-z]+)>/', $bits, $droppedTags, PREG_SET_ORDER);
+                if (!empty($droppedTags)) {
+                    foreach ($droppedTags as $closingTag) {
+                        if (!in_array($closingTag[1], $openTags)) {
+                            array_unshift($openTags, $closingTag[1]);
+                        }
+                    }
+                }
+            }
+            $truncate = mb_substr($truncate, 0, $spacepos);
+        }
+    }
+    $truncate .= $ending;
 
-	if ($html) {
-		foreach ($openTags as $tag) {
-			$truncate .= '</'.$tag.'>';
-		}
-	}
+    if ($html) {
+        foreach ($openTags as $tag) {
+            $truncate .= '</'.$tag.'>';
+        }
+    }
 
-	return $truncate;
+    return (!$keepTags ? strip_tags($truncate) : $truncate);
 }
 
 /**
@@ -673,15 +700,14 @@ function html_decode_utf8($string){
 	return htmlspecialchars(html_entity_decode($string), ENT_QUOTES, 'UTF-8');
 }
 
-//Permet de checker si l'élément est
-//setté et non vide
-function set_not_empty($element){
-	return (isset($element) && !empty($element));
-}
-//Permet de checker si l'élément est
-//non setté ou vide
-function not_set_empty($element){
-	return (!isset($element) || empty($element));
+//Convertit la première lettre de la chaine $string
+//en majuscule et le reste de la chaine en minuscule UTF-8
+//pour l'affichage HTML
+function mb_ucfirst($string,$encoding = 'UTF-8'){
+	$length = mb_strlen($string, $encoding);
+    $firstChar = mb_substr($string, 0, 1, $encoding);
+    $rest = mb_substr($string, 1, $length - 1, $encoding);
+    return mb_strtoupper($firstChar, $encoding).$rest;
 }
 
 // Permet de mettre au bon format le n° de 
@@ -851,7 +877,7 @@ function delete_folder_tree($dir, $selfDestroy=false) {
 	foreach ($files as $file) { 
 		(is_dir("$dir/$file")) ? delete_folder_tree("$dir/$file") : unlink("$dir/$file"); 
 	} 
-	if($selfDestroy) return delete_folder_tree($dir); 
+	if($selfDestroy) return rmdir($dir); 
 }
 
 //Normalise les caractères un peu spéciaux
@@ -1107,7 +1133,7 @@ function template($stream,$data){
     $stream = preg_replace_callback('/{{([^\/\:\;\?}]*)}}/',function($matches) use ($data) {
         $key = $matches[1];
         
-        if (isset($data[$key]) ) return $data[$key];
+        if (isset($data[$key]) && is_string($data[$key])) return $data[$key];
 
         $value = '';
         $attributes = explode('.',$key);
@@ -1118,7 +1144,7 @@ function template($stream,$data){
         	$value = $current;
         }
 
-        return $value;
+        return is_string($value) ? $value : '';
     },$stream); 
   
     return $stream;
@@ -1200,7 +1226,7 @@ function month_name($month){
 //Retourne le nom complet d'un jour en fonction de son numéro (1 : lundi,..., 7 :dimanche)
 function day_name($day){
     $translates = array('Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi','Dimanche');
-    return $translates[$day-1];
+    return isset($translates[$day-1])? $translates[$day-1]:$day;
 }
 
 
@@ -1301,7 +1327,6 @@ function escape_json_string($value) { # list from www.json.org: (\b backspace, \
     return $result;
 }
 
-
 if( !function_exists('apache_request_headers') ) {
 	///
 	function apache_request_headers() {
@@ -1334,5 +1359,4 @@ function mt_basename($path){
 }
 
 
-?>
-
+?>

+ 19 - 13
header.php

@@ -17,11 +17,12 @@ if(($page=='index.php' || $page==basename(ROOT_URL) || $page=='') && !isset($_['
 
 $cssModule = array();
 foreach(array('module','page','section') as $term){
-	if(isset($_[$term])) $cssModule[]= preg_replace('/([^a-z])/i', '-',$term.'-'.$_[$term]);
+	if(isset($_[$term])) $cssModule[] = preg_replace('/([^a-z])/i', '-',$term.'-'.$_[$term]);
 }
+if(empty($cssModule) && ($page=='index.php' || $page==basename(ROOT_URL) || $page=='')) $cssModule[] = 'module-index';
 
 if(!isset($_['admin_login']) && file_exists('enabled.maintenance') && !$myUser->connected()){
-    $_['error'] ? header('Location: maintenance.php?error='.$_['error']) : header('Location: maintenance.php');
+    isset($_['error']) && !empty($_['error']) ? header('Location: maintenance.php?error='.$_['error']) : header('Location: maintenance.php');
     exit();
 } else {
 	$mainMenu = array();
@@ -67,8 +68,12 @@ if(!isset($_['admin_login']) && file_exists('enabled.maintenance') && !$myUser->
 		<link href="<?php echo $mediaRoot ?>/css/trumbowyg.min.css" rel="stylesheet">
 		<link href="<?php echo $mediaRoot ?>/css/trumbowyg.table.css" rel="stylesheet">
 		<link href="<?php echo $mediaRoot ?>/css/trumbowyg.colors.css" rel="stylesheet">
-		<!-- Lato font-->
-		<link href="https://fonts.googleapis.com/css?family=Lato:300,400" rel="stylesheet">
+
+		<?php if(!$conf->get('offline_mode')): ?>
+			<!-- Lato font-->
+			<link href="https://fonts.googleapis.com/css?family=Lato:300,400" rel="stylesheet">
+		<?php endif; ?>
+
 		<!-- Custom styles for this template -->
 		<link href="<?php echo $mediaRoot ?>/css/main.css?v=<?php echo $cacheVersion; ?>" rel="stylesheet">
 		<!-- Plugin css files -->
@@ -131,24 +136,25 @@ if(!isset($_['admin_login']) && file_exists('enabled.maintenance') && !$myUser->
 							<?php endif;?>
 						</button>
 						<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
-	
 							<?php if (!$myUser->connected()): ?>
 							<div class="dropdown-item login-item" > 
-								<form id="loginForm" method="post" action="action.php?action=login" class="form-inline mt-mb-0">
+								<form id="loginForm" data-action="login" class="form-inline mt-mb-0">
 									<label for="login">Identifiant</label>
-									<input name="login" id="login" maxlength="64" class="form-control w-100 pb-1" type="text" required="required" autofocus="true">
+									<input name="login" id="login" maxlength="64" class="form-control form-control-sm w-100 pb-1" type="text" required="required" autofocus="true">
 									<label for="password">Mot de passe</label> 
-									<input data-type="password" name="password" id="password" class="form-control w-100" type="password" required="required">
+									<input data-type="password" name="password" id="password" class="form-control form-control-sm w-100" type="password" required="required">
 									<div class="form-check w-100 mt-2 mb-1">
-                                              <input class="form-check-input" data-type="checkbox" type="checkbox" id="rememberMe" name="rememberMe">
-									      <label class="form-check-label" for="rememberMe">Se souvenir de moi</label>
+								    	<input class="form-check-input" data-type="checkbox" type="checkbox" id="rememberMe" name="rememberMe">
+										<label class="form-check-label" for="rememberMe">Se souvenir de moi</label>
 									</div>
-									<input class="btn btn-success btn-small w-100" type="submit" value="Connexion" onclick="toggle_preloader_to('#loginRequest', 'div.container-fluid');">
-									<a href="account.lost.php" class="d-block w-100 text-center mt-2 mb-1">Mot de passe oublié ?</a>
+									<div class="btn btn-success btn-sm w-100" id="login-button" onclick="core_login('body>.container-fluid')">Se connecter</div>
+									<?php if($conf->get('password_allow_lost')): ?>
+										<a href="account.lost.php" class="d-block w-100 text-center mt-2 mb-1 btn-lost-account">Mot de passe oublié ?</a>
+									<?php endif; ?>
 								</form>
 							</div>
 							<?php else: ?>
-								<div class="font-weight-bold text-primary p-2 text-center"><?php echo $myUser->fullName(); ?></div>
+								<div class="font-weight-bold text-primary p-2 text-center user-fullname"><?php echo $myUser->fullName(); ?></div>
 							<?php foreach($userMenu as $item): ?>
 								<?php if(isset($item['custom'])):
 									echo $item['custom'];

+ 106 - 0
img/logo.svg

@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 143.2 85.7" style="enable-background:new 0 0 143.2 85.7;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#FFFFFF;}
+</style>
+<g>
+	<path class="st0" d="M68.7,82.3c-1.7,0-5.8,0-13.8,0c-1.9,0-3.8,0.3-4.7-0.6c-0.9-1-0.7-3.2-0.7-8.1V3.7c0-0.4,0-1.1,0.2-1.5
+		c0.7-1,0.9-0.9,2.1-1c18-0.3,24.6,0,24.6,0c1.2,0.3,1.2,0.5,1.2,8.7c0,9.7-0.1,10.2-1.4,10c0,0,0.3,0.1-0.8-0.2
+		c-0.4-0.1-4.8-0.2-5.1,0.3c-0.5,0.9-0.5,0.7-0.5,2.5v6.9c0,1.2-0.2,2.7,0.6,3.7c0.7,0.8,3.4,0.7,4.4,0.8c1.2,0.2,1.1,0.7,1.1,10
+		c0,8.7,0.2,9.3-1.1,9.1c-0.8-0.1-3.8,0-4.2,0.2c-0.9,0.7-0.7,2.1-0.7,3.9V51v28.9C69.8,81.2,69.1,82.3,68.7,82.3"/>
+	<path class="st0" d="M135.7,41.8c0.4-0.5,5.3-1.4,5.3-10.5V19.4c0-11.4-4.8-18.2-12.3-18.2c-3.1,0-8.7,0-11,0
+		c-2.4,0-2.4-0.1-2.4,3.6v37v0.1v37c0,3.7,0,3.6,2.4,3.6c2.3,0,7.9,0,11,0c7.5,0,12.3-6.8,12.3-18.2V52.4c0-9.1-4.9-10.1-5.3-10.5
+		C135.6,41.9,135.6,41.9,135.7,41.8C135.6,41.8,135.6,41.8,135.7,41.8"/>
+	<path class="st0" d="M111.1,81.1c-4-32.5-4.7-44.5-8.7-77.4c-0.4-3-4-2.7-6.2-2.7c-2.1,0-9.8-0.7-10,1.1
+		c-3.6,33.3-4.6,43.8-8.8,78.9c-0.1,1.2,5.6,1.2,7,1.2h5.1c0.2,0,1.3-0.2,1.4-1.4c0.1-1.1,1.8-12.5,1.8-12.5s0.1-0.3,0.4-0.3
+		c0.3,0,2.4-0.2,2.6,0.1c0.2,0.4,1.4,9.8,1.4,9.8s0.5,3.7,0.7,4c0.1,0.2,0.4,0.3,0.6,0.3h6.1C106.7,82.4,111.3,82.5,111.1,81.1"/>
+	<path class="st0" d="M2.5,59.5h0.5v-0.7c0-1.1,0.7-1.6,1.5-1.6c0.4,0,0.8,0.1,1,0.3v0.2L5,58.1C4.9,58,4.7,58,4.5,58
+		c-0.4,0-0.7,0.2-0.7,0.9v0.6h0.6v0.7H3.8v2.9H2.9v-2.9H2.5V59.5z"/>
+	<path class="st0" d="M7.4,61.5H6.5c-0.5,0-0.6,0.3-0.6,0.5c0,0.2,0.2,0.5,0.7,0.5C7.2,62.4,7.4,61.9,7.4,61.5 M8.2,62.3h0.3v0.8
+		H7.9l-0.3-0.3c-0.2,0.2-0.5,0.4-1,0.4c-0.9,0-1.5-0.5-1.5-1.2c0-0.4,0.2-1.1,1.5-1.1h0.8v-0.2c0-0.3-0.4-0.5-0.7-0.5
+		c-0.3,0-0.5,0.1-0.7,0.4l-0.7-0.3c0.2-0.5,0.8-0.8,1.4-0.8c0.7,0,1.5,0.3,1.5,1.1V62.3z"/>
+	<path class="st0" d="M12.4,61.3c0-0.6-0.5-1.1-1.1-1.1c-0.6,0-1,0.5-1,1.1c0,0.6,0.5,1.1,1,1.1C11.9,62.3,12.4,61.9,12.4,61.3
+		 M9.5,63.1v-5.6l0.3-0.3h0.6v2.6c0.2-0.2,0.6-0.4,1-0.4c1,0,1.9,0.9,1.9,1.9c0,1-0.8,1.9-1.9,1.9c-0.6,0-0.9-0.3-1-0.4L10,63.1H9.5
+		z"/>
+	<path class="st0" d="M15.1,63.1h-0.8v-2.9h-0.3v-0.7h0.7l0.4,0.4c0.2-0.3,0.5-0.5,0.9-0.5c0.1,0,0.2,0,0.4,0.1L16,60.3
+		c-0.1,0-0.2,0-0.3,0c-0.3,0-0.7,0.2-0.7,0.6V63.1z"/>
+	<path class="st0" d="M17.3,57.4c0.3,0,0.6,0.3,0.6,0.6c0,0.3-0.3,0.6-0.6,0.6c-0.3,0-0.6-0.3-0.6-0.6C16.7,57.7,17,57.4,17.3,57.4
+		 M17.8,63.1H17v-2.9h-0.3v-0.7h0.8l0.3,0.3V63.1z"/>
+	<path class="st0" d="M21.6,61.3c0-0.6-0.5-1.1-1-1.1c-0.6,0-1.1,0.5-1.1,1.1c0,0.6,0.5,1.1,1.1,1.1C21.1,62.3,21.6,61.9,21.6,61.3
+		 M21.5,65v-2.3c-0.2,0.2-0.5,0.4-1,0.4c-1,0-1.9-0.9-1.9-1.9c0-1,0.8-1.9,1.9-1.9c0.5,0,0.8,0.2,1,0.4l0.3-0.3h0.5V65H21.5z"/>
+	<path class="st0" d="M23.5,61.5v-2h0.8v2c0,0.6,0.3,0.9,0.7,0.9c0.4,0,0.7-0.3,0.7-0.9v-2h0.8v2c0,1.1-0.7,1.7-1.6,1.7
+		C24.3,63.1,23.5,62.6,23.5,61.5"/>
+	<path class="st0" d="M28.3,60.8h1.7c-0.1-0.4-0.4-0.7-0.9-0.7C28.8,60.1,28.4,60.4,28.3,60.8 M30.1,61.9l0.7,0.4
+		c-0.3,0.5-0.8,0.9-1.5,0.9c-1,0-1.8-0.9-1.8-1.9c0-1,0.7-1.9,1.8-1.9c1,0,1.7,0.9,1.7,1.8l-0.3,0.3h-2.3c0,0.5,0.5,0.9,0.9,0.9
+		C29.6,62.4,29.9,62.2,30.1,61.9"/>
+	<path class="st0" d="M5.6,69.8c0-0.6-0.5-1.1-1-1.1c-0.6,0-1.1,0.5-1.1,1.1c0,0.6,0.5,1.1,1.1,1.1C5.1,70.9,5.6,70.4,5.6,69.8
+		 M5.5,68.3V66l0.3-0.3h0.6v5h0.3v0.8H5.9l-0.3-0.4c-0.2,0.2-0.5,0.4-1.1,0.4c-1,0-1.9-0.9-1.9-1.9c0-1,0.8-1.9,1.9-1.9
+		C5,67.9,5.3,68.1,5.5,68.3"/>
+	<path class="st0" d="M8,69.3h1.7c-0.1-0.4-0.4-0.7-0.9-0.7C8.4,68.6,8.1,68.9,8,69.3 M9.7,70.4l0.7,0.4c-0.3,0.5-0.8,0.9-1.5,0.9
+		c-1,0-1.8-0.9-1.8-1.9c0-1,0.7-1.9,1.8-1.9c1,0,1.7,0.9,1.7,1.8L10.3,70H8c0,0.5,0.5,0.9,0.9,0.9C9.2,70.9,9.5,70.7,9.7,70.4"/>
+	<path class="st0" d="M16.3,69.8c0-0.6-0.5-1.1-1.1-1.1c-0.6,0-1,0.5-1,1.1c0,0.6,0.5,1.1,1,1.1C15.8,70.9,16.3,70.4,16.3,69.8
+		 M13.4,71.6V66l0.3-0.3h0.6v2.6c0.2-0.2,0.6-0.4,1-0.4c1,0,1.9,0.9,1.9,1.9c0,1-0.8,1.9-1.9,1.9c-0.6,0-0.9-0.3-1-0.4l-0.4,0.3
+		H13.4z"/>
+	<path class="st0" d="M20.6,69.8c0-0.6-0.4-1.1-1-1.1c-0.6,0-1,0.5-1,1.1c0,0.6,0.4,1.1,1,1.1C20.1,70.9,20.6,70.4,20.6,69.8
+		 M21.4,69.8c0,1.1-0.9,1.9-1.9,1.9c-1,0-1.9-0.8-1.9-1.9c0-1.1,0.9-1.9,1.9-1.9C20.6,67.9,21.4,68.7,21.4,69.8"/>
+	<path class="st0" d="M23.2,71.6h-0.8v-2.9h-0.3V68h0.7l0.4,0.4c0.2-0.3,0.5-0.5,0.9-0.5c0.1,0,0.2,0,0.4,0.1l-0.2,0.9
+		c-0.1,0-0.2,0-0.3,0c-0.3,0-0.7,0.2-0.7,0.6V71.6z"/>
+	<path class="st0" d="M27.7,69.8c0-0.6-0.5-1.1-1-1.1c-0.6,0-1.1,0.5-1.1,1.1c0,0.6,0.5,1.1,1.1,1.1C27.3,70.9,27.7,70.4,27.7,69.8
+		 M27.7,68.3V66l0.3-0.3h0.6v5h0.3v0.8H28l-0.3-0.4c-0.2,0.2-0.5,0.4-1,0.4c-1,0-1.9-0.9-1.9-1.9c0-1,0.8-1.9,1.9-1.9
+		C27.1,67.9,27.4,68.1,27.7,68.3"/>
+	<path class="st0" d="M30.1,69.3h1.7c-0.1-0.4-0.4-0.7-0.9-0.7C30.6,68.6,30.2,68.9,30.1,69.3 M31.9,70.4l0.7,0.4
+		c-0.3,0.5-0.8,0.9-1.5,0.9c-1,0-1.8-0.9-1.8-1.9c0-1,0.7-1.9,1.8-1.9c1,0,1.7,0.9,1.7,1.8L32.4,70h-2.3c0,0.5,0.5,0.9,0.9,0.9
+		C31.4,70.9,31.7,70.7,31.9,70.4"/>
+	<path class="st0" d="M35.7,70h-0.9c-0.5,0-0.6,0.3-0.6,0.5c0,0.2,0.2,0.5,0.7,0.5C35.5,71,35.7,70.4,35.7,70 M36.5,70.8h0.3v0.8
+		h-0.6l-0.3-0.3c-0.2,0.2-0.5,0.4-1,0.4c-0.9,0-1.5-0.5-1.5-1.2c0-0.4,0.2-1.1,1.5-1.1h0.8v-0.2c0-0.3-0.4-0.5-0.7-0.5
+		c-0.3,0-0.5,0.1-0.7,0.4l-0.7-0.3c0.2-0.5,0.8-0.8,1.4-0.8c0.7,0,1.5,0.3,1.5,1.1V70.8z"/>
+	<path class="st0" d="M37.7,70v-2h0.8v2c0,0.6,0.3,0.9,0.7,0.9c0.4,0,0.7-0.3,0.7-0.9v-2h0.8v2c0,1.1-0.7,1.7-1.6,1.7
+		C38.4,71.7,37.7,71.1,37.7,70"/>
+</g>
+<polygon class="st0" points="44.1,71.6 43.3,70.5 42.5,71.6 41.5,71.6 42.8,69.8 41.5,68 42.5,68 43.3,69.1 44.1,68 45,68 
+	43.7,69.8 45,71.6 "/>
+<g>
+	<path class="st0" d="M3.9,80.1H3v-2.9H2.7v-0.7h0.7l0.3,0.3c0.3-0.3,0.7-0.4,1-0.4c0.4,0,0.7,0.2,1,0.5c0.3-0.3,0.7-0.5,1.2-0.5
+		c0.7,0,1.3,0.5,1.3,1.5v2.2H7.4v-2.2c0-0.5-0.3-0.7-0.6-0.7c-0.4,0-0.7,0.3-0.7,0.6v2.3H5.2v-2.2c0-0.5-0.3-0.7-0.6-0.7
+		c-0.4,0-0.7,0.2-0.7,0.6V80.1z"/>
+	<path class="st0" d="M9.8,77.8h1.7c-0.1-0.4-0.4-0.7-0.9-0.7C10.3,77.1,9.9,77.4,9.8,77.8 M9.8,75.7c0.3-0.3,0.7-0.8,0.9-1.1
+		c0.1-0.1,0.2-0.2,0.4-0.2c0.1,0,0.1,0,0.2,0c0.2,0.1,0.3,0.3,0.3,0.4c0,0.1-0.1,0.3-0.2,0.4c-0.1,0.1-1.2,0.6-1.2,0.6L9.8,75.7z
+		 M11.6,78.9l0.7,0.4c-0.3,0.5-0.8,0.9-1.5,0.9c-1,0-1.8-0.9-1.8-1.9c0-1,0.7-1.9,1.8-1.9c1,0,1.7,0.9,1.7,1.8l-0.3,0.3H9.8
+		c0,0.5,0.5,0.9,0.9,0.9C11,79.4,11.4,79.2,11.6,78.9"/>
+</g>
+<polygon class="st0" points="12.9,76.5 13.3,76.5 13.3,75.4 13.6,75.1 14.2,75.1 14.2,76.5 14.8,76.5 14.8,77.2 14.2,77.2 
+	14.2,79.3 14.5,79.3 14.5,80.1 13.7,80.1 13.3,79.7 13.3,77.2 12.9,77.2 "/>
+<g>
+	<path class="st0" d="M16.6,80.1h-0.8v-2.9h-0.3v-0.7h0.7l0.4,0.4c0.2-0.3,0.5-0.5,0.9-0.5c0.1,0,0.2,0,0.4,0.1l-0.2,0.9
+		c-0.1,0-0.2,0-0.3,0c-0.3,0-0.7,0.2-0.7,0.6V80.1z"/>
+	<path class="st0" d="M21,78.3c0-0.6-0.4-1.1-1-1.1c-0.6,0-1,0.5-1,1.1c0,0.6,0.4,1.1,1,1.1C20.6,79.4,21,78.9,21,78.3 M21.9,78.3
+		c0,1.1-0.9,1.9-1.9,1.9c-1,0-1.9-0.8-1.9-1.9c0-1.1,0.9-1.9,1.9-1.9C21,76.4,21.9,77.2,21.9,78.3"/>
+	<path class="st0" d="M25.7,78.3c0-0.6-0.5-1.1-1.1-1.1c-0.6,0-1.1,0.5-1.1,1.1c0,0.6,0.5,1.1,1.1,1.1
+		C25.3,79.4,25.7,78.9,25.7,78.3 M22.9,82v-4.8h-0.3v-0.7h0.7l0.4,0.4c0.2-0.2,0.6-0.5,1.1-0.5c1,0,1.9,0.9,1.9,1.9
+		c0,1-0.8,1.9-1.9,1.9c-0.5,0-0.9-0.3-1-0.4V82H22.9z"/>
+	<path class="st0" d="M30.1,78.3c0-0.6-0.4-1.1-1-1.1c-0.6,0-1,0.5-1,1.1c0,0.6,0.4,1.1,1,1.1C29.6,79.4,30.1,78.9,30.1,78.3
+		 M30.9,78.3c0,1.1-0.9,1.9-1.9,1.9c-1,0-1.9-0.8-1.9-1.9c0-1.1,0.9-1.9,1.9-1.9C30,76.4,30.9,77.2,30.9,78.3"/>
+</g>
+<polygon class="st0" points="31.8,74.5 32,74.3 32.6,74.3 32.6,79.3 32.9,79.3 32.9,80.1 32.1,80.1 31.8,79.7 "/>
+<g>
+	<path class="st0" d="M34.3,77.8H36c-0.1-0.4-0.4-0.7-0.9-0.7C34.7,77.1,34.4,77.4,34.3,77.8 M36,78.9l0.7,0.4
+		c-0.3,0.5-0.8,0.9-1.5,0.9c-1,0-1.8-0.9-1.8-1.9c0-1,0.7-1.9,1.8-1.9c1,0,1.7,0.9,1.7,1.8l-0.3,0.3h-2.3c0,0.5,0.5,0.9,0.9,0.9
+		C35.5,79.4,35.8,79.2,36,78.9"/>
+	<path class="st0" d="M9,1.5c-0.5,0-2.5,0-4.1,0C1.8,1.4,2.2,1.8,2.2,2.8v45.5c0,0.8-0.1,1.8,0.2,2.3c0.2,0.2,0.5,0.3,0.9,0.3
+		c0.3,0,0.5,0,0.8,0c1.9,0,10.2,0,12.4,0c0.6,0,0.6-0.1,0.6-5.5c0-4.6,0-5-0.5-5c0,0-0.1,0-0.1,0c-1.4,0.1-3.3,0.3-4.6,0.4
+		c-0.1,0-0.3,0-0.4,0c-0.4,0-0.8,0-0.9-0.2c-0.2-0.2-0.2-3.7-0.2-4.6V2.4C10.2,1.7,9.6,1.5,9,1.5 M8.6,3v32.9c0,1.9,0,5.5,0.9,6.3
+		c0.4,0.4,1,0.5,1.8,0.5c0.2,0,0.4,0,0.5,0l3.6-0.3c0,0.9,0,2,0,2.9c0,1.1,0,2.6,0,3.5l-5.3,0l-6.1,0c-0.1,0-0.2,0-0.3,0
+		c0-0.1,0-0.1,0-0.1c0-0.2,0-0.3,0-0.5V3C4,2.8,4.5,2.8,5.6,2.9C5.9,2.9,8.4,2.9,8.6,3"/>
+	<path class="st0" d="M34.1,49.1l3.5,0c0.6,0,2.1,0,2.7-0.2c-1.1-9.4-1.8-15.9-2.5-22.3c-0.7-6.4-1.4-13.1-2.5-22.7
+		c-0.1-0.7-0.5-0.8-2.2-0.8c-0.2,0-0.3,0-0.5,0c-0.3,0-0.8,0-1.3,0c-0.9,0-3.3-0.1-4,0.1c-0.5,4.4-0.9,8.1-1.2,11.5
+		c-1.2,11.2-2,18.6-3.9,34.1c0.4,0.1,1.3,0.2,3.2,0.2h2.9c0,0,0,0,0,0c0,0,0,0,0,0c0.1-0.7,1-6.7,1.1-7.4l0-0.1l0-0.1
+		c0.1-0.3,0.4-0.7,0.9-0.8c0.1,0,0.5,0,0.9-0.1c0.5,0,1.2,0,1.5,0.5c0.1,0.2,0.2,0.4,0.9,6.1C33.9,48,34,48.7,34.1,49.1 M37.8,50.8
+		c-0.1,0-0.2,0-0.2,0l-0.3,0h-3.7l-0.1,0c-0.2-0.1-0.6-0.2-0.8-0.5c-0.1-0.2-0.2-0.3-0.6-2.7c-0.3-2-0.6-4.2-0.7-5.2
+		c-0.1,0-0.2,0-0.3,0c-0.3,2.1-1,6.3-1,6.8c-0.1,1-0.8,1.5-1.5,1.6l-0.1,0h-3c-3.3,0-4.2-0.3-4.7-0.8c-0.2-0.2-0.3-0.5-0.3-0.8l0,0
+		c1.9-15.8,2.7-23.3,3.9-34.5c0.4-3.4,0.8-7.2,1.3-11.7c0.2-1.6,2.9-1.5,5.7-1.4c0.5,0,0.9,0,1.2,0c0.1,0,0.3,0,0.4,0
+		c1.2,0,3.6-0.1,3.8,2.3c1.2,9.6,1.9,16.3,2.5,22.7c0.7,6.4,1.4,13.1,2.5,22.6v0c0,0.3-0.1,0.7-0.3,0.9C41.2,50.7,40,50.8,37.8,50.8
+		"/>
+</g>
+</svg>

BIN
img/logo/default-favicon.png


BIN
img/logo/default-logo.dark.png


BIN
img/logo/default-logo.png


+ 22 - 17
install.php

@@ -1,7 +1,7 @@
 <?php
 
 try {
-	date_default_timezone_set('Europe/paris');
+	date_default_timezone_set('Europe/Paris');
 	mb_internal_encoding('UTF-8');
 	require_once(__DIR__.'/function.php');
 	spl_autoload_register('app_autoloader');
@@ -10,6 +10,11 @@ try {
 	require_once('class/Plugin.class.php');
 	$entityFolder = __DIR__.'/class/';
 	
+
+	if(file_exists(__DIR__.DIRECTORY_SEPARATOR.'.git') && !file_exists(__DIR__.DIRECTORY_SEPARATOR.'.git'.DIRECTORY_SEPARATOR.'.htaccess')){
+		file_put_contents(__DIR__.DIRECTORY_SEPARATOR.'.git'.DIRECTORY_SEPARATOR.'.htaccess', 'deny for all');
+	}
+	
 	?>
 
 	<!DOCTYPE html>
@@ -37,12 +42,15 @@ try {
 	<body>
 		<!-- Fixed navbar -->
 		<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
-			<a class="navbar-brand" style="background: url('img/logo/default-logo.png') no-repeat 0 center; padding-left:40px;" href="index.php"> Installateur</a>
-		
+			<a class="navbar-brand" href="install.php">Installateur</a>
 			<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
 				<span class="navbar-toggler-icon"></span>
 			</button>
-		
+			<div class="collapse navbar-collapse" id="navbarCollapse">
+				<ul class="navbar-nav mr-auto">
+					<li class="nav-item active"><a class="nav-link" href="install.php">Installation <span class="sr-only">(current)</span></a></li>
+				</ul>
+			</div>
 		</nav>
 
 		<!-- Begin page content -->
@@ -92,7 +100,7 @@ try {
 			//install entities
 			Entity::install(__ROOT__.'class');
 
-			global $conf;
+			global $conf,$myUser;
 			$conf = new Configuration();
 			$conf->getAll();
 
@@ -112,13 +120,14 @@ try {
 			$admin = new User();
 			$admin->login = 'admin';
 			$admin->password = User::password_encrypt('admin');
-			$admin->firstname = 'Chuck';
-			$admin->name = 'Norris'; 
+			$admin->firstname = 'Administrateur';
+			$admin->name = 'SYS1'; 
 			$admin->superadmin = 1; 
 			$admin->rank = $rank->id;
 			$admin->state = User::ACTIVE;
 			$admin->save();
 			$_SESSION['currentUser'] = serialize($admin);
+			$myUser = $admin;
 
 			$userfirmrank = new UserFirmRank();
 			$userfirmrank->user = $admin->login;
@@ -138,15 +147,11 @@ try {
 				$right->save();
 			}
 			//Activation des plugins par défaut
-			foreach (array('fr.idleman.hackpoint','fr.idleman.wiki','fr.idleman.dashboard','fr.idleman.notification','fr.idleman.customiser') as  $plugin) {
+			foreach (array('fr.sys1.factory','fr.sys1.dashboard','fr.sys1.notification','fr.sys1.navigation') as  $plugin) {
 				Plugin::state($plugin,true);
-			} 
-
-			$conf->put('core_theme','/plugin/customiser/theme/hackpoint/main.css');
-
-			?>
+			} ?>
 
-			<div class="alert alert-success alert-dismissable mt-3">
+			<div class="alert alert-success alert-dismissable">
 				<button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>
 				<strong>Succès!</strong> La base est bien installée, l'utilisateur par défaut est <code>admin:admin</code>, pensez à changer le mot de passe rapidemment. <br>
 			</div>
@@ -159,8 +164,8 @@ try {
 			$parts = explode('?',$root);
 			$root = array_shift($parts);
 			?>
-			<div class="row mt-3">
-				<form class="col-md-6" action="install.php" method="POST">
+			<div class="row">
+				<form class="col-md-3" action="install.php" method="POST">
 					<h3>Installation</h3>
 					<p>Merci de bien vouloir remplir les champs ci-dessous</p>
 					<label for="entity">Base de donnée</label>
@@ -200,7 +205,7 @@ try {
 
 	<footer class="footer">
 		<div class="container">
-			<span class="text-muted">Installateur by <a href="mailto:valentin.carruesco@idleman.fr">@valentin carruesco<a></span>
+			<span class="text-muted">Installateur by <a href="mailto:valentin.carruesco@sys1.fr">@valentin carruesco<a></span>
 		</div>
 	</footer>
 

+ 1 - 1
maintenance.php

@@ -35,7 +35,7 @@ if(file_exists('disabled.maintenance')){
 				</div>
 			</div>
 			<p>Merci de revenir dans quelques instants</p>
-			<p><i>L'équipe IdleCorp</i></p>
+			<p><i>L'équipe Sys1</i></p>
 			<small class="admin-access">
 				<a href="index.php?admin_login=1" class="admin-link">- Accès technicien -</a>
 			</small>

+ 58 - 0
plugin/customiser/theme/hackpoint/main.css

@@ -191,4 +191,62 @@ div[data-type="dropzone"] > ul > li {
     padding: 3px;
     border-bottom: 1px solid #2f2f2f;
     background: #343a40;
+}
+
+/* document */
+
+.hackpoint .document-container .tree-panel {
+    width: 15%;
+    order: 1;
+    padding: 15px;
+    color: #d2d2d2;
+    background-color: #343940;
+    /* background-color: #ddedff; */
+    font-size: 15px;
+    border-right: 1px solid #30353c;
+    height: calc(100vh - 100px);
+    overflow-y: auto;
+    resize: horizontal;
+}
+
+
+.hackpoint .file-module > table tbody tr {
+    border-bottom: 1px solid #333a42;
+    color: #dedede;
+}
+.hackpoint .file-module > table thead tr {
+    border-bottom: 1px solid #333a42;
+}
+
+.hackpoint .file-module {
+    border-top: 1px solid #333a42;
+}
+.hackpoint .document-container .file-elements-list tbody tr:hover {
+    background-color: #343940;
+}
+.hackpoint .document-container .file-elements-list .element-focused {
+    font-weight: bold;
+    background-color: #343940;
+}
+.hackpoint .detail-panel .detail-thumbnail {
+    background-color:  #343940;
+}
+.hackpoint .detail-panel .detail-thumbnail .thumbnail-preloader {
+    background-color: #343940;
+    color: #cecece;
+}
+.hackpoint .document-container .detail-panel {
+
+    border-right: 1px solid #333a42;
+}
+.hackpoint .file-panel .breadcrumb-module {
+    padding: 0 15px 0 15px;
+    margin: 5px 0;
+    color: #c5c5c5;
+}
+
+.hackpoint  .document-container .file-panel {
+   
+    border-right: 1px solid #333a42;
+   
 }

+ 519 - 0
plugin/document/Element.class.php

@@ -0,0 +1,519 @@
+<?php
+/**
+ * Define a fileor directory element.
+ * @author Valentin CARRUESCO
+ * @category Plugin
+ * @license copyright
+ */
+class Element extends Entity{
+	public $id,$path,$label,$type,$extension,$size,$creatorSite,$icon,$thumbnail,$childNumber;
+	protected $TABLE_NAME = 'document_element';
+	public $fields =
+	array(
+		'id' => 'key',
+		'path' => 'longstring',
+		'label' => 'string',
+		'type' => 'string',
+		'extension' => 'string',
+		'size' => 'string',
+		'creatorSite' => 'int'
+	);
+
+	//Récupération de la racine de la GED
+	public static function root(){
+		return File::dir().'documents'.SLASH;
+	}
+
+	public  function save(){
+		$this->path = str_replace('\\','/',$this->path);
+		parent::save();
+	}
+
+	//Récuperation du chemin ou du flux de thumbnail associé à l'élement
+	public function thumbnail(){
+		
+		if(!in_array($this->extension, array('jpg','png','jpeg','gif','bmp'))) return  $this->icon();
+
+		if(!file_exists(self::root().'.thumbnails')) mkdir(self::root().'.thumbnails',755,true);
+
+		
+		
+		$thumbname = str_replace(array('\\','./'),array('/',''),$this->path);
+		$thumbnail = self::root().'.thumbnails'.SLASH.base64_encode($thumbname).'.'.$this->extension;
+		
+		if(!file_exists($thumbnail)){
+			$osPath = File::convert_decoding($this->path);
+			copy(self::root().$osPath,$thumbnail);
+			Image::resize($thumbnail,200,200);
+		}
+		
+		$path ='data:image/'.$this->extension.';base64,'.base64_encode(file_get_contents($thumbnail));
+	
+		return $path;
+	}
+
+	//Récuperation du chemin di'one à l'élement
+	public function icon(){
+		$path = 'plugin/document/img/file-types/';
+		$icon = $this->type == 'directory' ? 'folder.svg' : 'document.svg';
+		
+		switch($this->extension){
+			case 'docx':
+			case 'doc':
+			case 'dot':
+			case 'dotx':
+				$icon = 'word.svg';
+			break;
+			case 'xls':
+			case 'xlsx':
+			case 'csv':
+				$icon = 'excel.svg';
+			break;
+			case 'ppt':
+			case 'pptx':
+				$icon = 'powerpoint.svg';
+			break;
+			case 'pdf':
+				$icon = 'pdf.svg';
+			break;
+			case 'mp3':
+			case 'wav':
+			case 'mp4':
+			case 'flac':
+				$icon = 'music.svg';
+			break;
+			case 'html':
+			case 'htm':
+				$icon = 'url.svg';
+			break;
+			case 'msg':
+				$icon = 'mail.svg';
+			break;
+			case 'psd':
+				$icon = 'photoshop.svg';
+			break;
+			case 'rss':
+				$icon = 'feed.svg';
+			break;
+			case 'mdb':
+				$icon = 'access.svg';
+			break;
+			case 'bat':
+			case 'sh':
+			case 'cmd':
+				$icon = 'gear.svg';
+			break;
+			case 'php':
+			case 'js':
+			case 'java':
+			case 'class':
+			case 'css':
+			case 'c':
+			case 'cpp':
+			case 'py':
+				$icon = 'code.svg';
+			break;
+			case 'jpg':
+			case 'jpeg':
+			case 'png':
+			case 'bmp':
+			case 'gif':
+			case 'svg':
+				$icon = 'image.svg';
+			break;
+		}
+
+		$path .= $icon;
+	
+		return $path;
+	}
+
+	//Récuperation d'un element base depuis un chemin physique, ou d'un objet element rensiegné a partir du path si non existant en base (prend du utf8 en entré)
+	public static function fromPath($path){
+		global $conf;
+		$element = new self();
+
+		//convertion utf8 ISO-8859-1
+		$osPath = File::convert_decoding($path);
+		$infos = pathinfo($path);
+		//Gestion label
+		
+		$element->label = mt_basename($path);
+		
+		$element->path = str_replace(self::root(),'',$path);
+		$element->type = is_dir($osPath) ? 'directory' : 'file';
+		$exists = file_exists($osPath);
+
+		if($element->type == 'directory'){
+			$path .= SLASH;
+			$fi = new FilesystemIterator($osPath, FilesystemIterator::SKIP_DOTS);
+			$element->childNumber = iterator_count($fi);
+			$element->size =  0;
+		} else { 
+			if(isset($infos['extension'])) $element->extension = $infos['extension'];
+			$element->size = !$exists ? 0 : filesize($osPath);
+		}
+
+		$element->updated =  !$exists ? 0 : filemtime($osPath);
+		$element->created =  !$exists ? 0 : filectime($osPath);
+		
+		$relativePath = trim(str_replace(Element::root(),'',$path),SLASH);
+		if ($baseElement = Element::load(array('path'=>str_replace('\\','/',$relativePath)   ))) {
+			$element->creator = $baseElement->creator;
+			$element->id = $baseElement->id;
+		}
+		$element->path = trim($element->path,SLASH);
+		return $element;
+	}
+	
+	//Récuperation de la liste des élements d'un repertoire (tiens compte des droits utilisateur) (attends de l'utf8)
+	public static function browse($scanned,$folderOnly = false){
+		global $myUser,$myFirm;
+		require_once(__DIR__.SLASH.'ElementRight.class.php');
+	
+		$osScanned = File::convert_decoding($scanned);
+		$elements = array();
+		User::check_access('document','read');
+
+		$toCheck = array();
+
+		//Récuperation du tableau des rang utilisateur sous forme d'id.
+		$ranks = array();
+		if(!$myUser->superadmin){
+			foreach($myUser->ranks[$myFirm->id] as $rank)	
+				$ranks[] = $rank->id;
+		}
+
+		$existingPathes = array();
+	
+		//Pour chaque fichier physique existant
+		foreach(glob($osScanned) as $osPath){
+			$osPath = str_replace(SLASH.'.'.SLASH, SLASH, $osPath);
+			$basePath = File::convert_encoding($osPath);
+			if((substr(mt_basename($osPath), 0,1)=='.') || ($folderOnly && !is_dir($osPath))) continue;
+
+			
+			//Récuperation/création de l'element objet correspondant (frompath n'accepte que l'utf8, on lui donne le basepath)
+			$line = Element::fromPath($basePath);
+
+			$existingPathes[] = $line->path;
+
+			//Si l'element correspondant n'existe pas en base, on le créé
+			if($line->id == ''){
+				$line->creator = 'anonymous';
+				$line->save();
+			}
+
+			//Si l'utilisateur n'est pas le créateur, pas un super admin et n'a pas le droit configure sur les documents on check les partages de droits
+			if($myUser->login != $line->creator && !$myUser->can('document','configure')  && !$myUser->superadmin){
+				$can = false;
+				$pathes = array();
+				$path = '';
+
+				//Récuperation des chemins dont on doit récuperer les droits :
+				// ex : pour le fichier a/b/c les chemins sont a, a/b, et a/b/c
+				foreach(explode(SLASH,$line->path) as $i=>$pathCrumb){
+					if($i!=0) $path.= SLASH;
+					$path.= $pathCrumb;
+					$pathes[] = str_replace("\\","\\\\",$path);
+				}
+				//On récupere les droits pour chaques chemin
+				foreach(ElementRight::staticQuery('SELECT er.*,el.path as '.Element::tableName().'_join_path  FROM {{table}} er LEFT JOIN '.Element::tableName().' el ON el.id=er.element WHERE 
+					( er.entity="all" OR (er.entity="user" AND er.uid=?) OR (er.entity="rank" AND er.uid IN ('.implode(',',$ranks).')  ) ) 
+					AND element IN(SELECT id from '.Element::tableName().' WHERE path IN ("'.implode('","',$pathes).'")) ',array($myUser->login),true,1) as $right){
+
+					$element = $right->join('element');
+					//Si les droits sont sur un dossier parent et son recursif OU si les droits sont sur le fichier concerné on continue la verification
+					if($right->element == $line->id || ($right->recursive && strpos($line->path, $element->path)!==false ) )
+						$can = true;
+				}
+				if(!$can) continue;
+			}
+			$elements[] = $line;
+		}
+
+
+		//Element::staticQuery('SELECT * FROM {{table}} WHERE `path` NOT IN ("'.implode('","',$existingPathes).'") ');
+
+
+
+		return $elements;
+	}
+
+	//Téléchargement d'un fichier ou d'un repertoire (prends de l'utf 8 en entrée)
+	public static function download($path){
+		$osPath = File::convert_decoding($path);
+
+		if(!file_exists($osPath)) return '';
+		$element = Element::fromPath($path);
+
+		if(!self::hasRight($element,'read')) throw new Exception("Permissions insuffisantes",403);
+
+		if(is_dir($osPath)){
+			$filename = rand().'-'.time().'.zip';
+			$filepath = sys_get_temp_dir().SLASH.$filename;
+			$zip = new ZipArchive;
+
+			$res = $zip->open($filepath, ZipArchive::CREATE);
+			if ($res === TRUE) {
+				$ressources = glob_recursive($osPath.SLASH.'*') ;
+				foreach($ressources as $i=>$resource){
+					if(is_dir($resource)) continue;
+					$filename = $resource;
+					$filename = File::convert_encoding($filename);
+					$filename =  str_replace($path,mt_basename($path),$filename);
+					$filename=iconv("UTF-8", "IBM850", $filename);
+					if(!$zip->addFromString($filename, file_get_contents($resource))) throw new Exception("Impossible de compresser le fichier ".$path);	
+				}
+
+				if(count($ressources)==0)
+					$zip->addEmptyDir('.');
+
+				$zip->close();
+			}
+			$stream = file_get_contents($filepath);
+			unlink($filepath); 
+		}else{
+			$stream = file_get_contents($osPath);
+		}
+		return $stream;
+	}
+
+	//Supression d'un répértoire ou d'un fichier en base et en physique (prends de l'utf 8 en entrée)
+	public static function remove($path){
+		global $myUser,$myFirm;
+		require_once(__DIR__.SLASH.'ElementRight.class.php');
+		$osPath = File::convert_decoding($path);
+		if(!file_exists($osPath)) return;
+		$element = Element::fromPath($path);
+		$dbPath = str_replace('\\','/',$element->path);
+		if(!self::hasRight($element,'edit')) throw new Exception("Permissions insuffisantes",403);
+	
+		if(is_dir($osPath)) {
+			self::remove_dir_recursive($osPath);
+			Element::staticQuery('DELETE FROM {{table}} WHERE `path` = ? OR `path` LIKE ?',array($dbPath,$dbPath.'/%'));
+		} else {
+			unlink($osPath);
+			Element::delete(array('path'=>$dbPath));
+		}
+		ElementRight::delete(array('element'=>$element->id));
+	}
+
+	//Copie d'un répértoire ou d'un fichier en base et en physique (prends de l'utf 8 en entrée)
+	public static function copy($path,$to){
+		global $conf, $myUser;
+
+		$osPath = File::convert_decoding($path);
+		$osTo = File::convert_decoding($to);
+
+		if(!file_exists($osPath)) return;
+		$element = Element::fromPath($path);
+		if(file_exists($osTo)) return false;
+
+		$parentPathTo = dirname($to);
+		$parentOsPathTo = dirname($osTo);
+		if(!file_exists($parentOsPathTo))  throw new Exception("Dossier de destination inexistant",404);
+		$parentTo = Element::fromPath($parentPathTo);
+		if(!self::hasRight($parentTo,'read')) throw new Exception("Permissions insuffisantes",403);
+		if(!self::hasRight($element,'read')) throw new Exception("Permissions insuffisantes",403);
+
+		$dbPath = str_replace('\\','/',$element->path);
+
+		if($element->type == 'directory'){
+			$element->path = $element->path;
+			//Si c'est un dossier, on récupere tous les élements commendant par ce nom afin de gerer leurs chemins
+			$baseElement = Element::staticQuery('SELECT * FROM {{table}} WHERE `path` = ? OR `path` LIKE ?',array($dbPath,$dbPath.'/%'), true);
+		} else {
+			$baseElement = Element::load(array('path'=>$dbPath));
+			if(is_object($baseElement) && $baseElement->id != 0) $element->id = $baseElement->id;
+		}
+		
+		if(!copy($osPath,$osTo)) throw new Exception('Erreur lors de la copie...');
+		$newelement = Element::fromPath($to);
+
+		if(is_array($baseElement)){
+			foreach ($baseElement as $elem) {
+				if($elem->path == $dbPath) $elem->label = $element->label;
+				$elem->path = str_replace($dbPath, $newelement->path, $elem->path);
+				$elem->id = 0;
+				$elem->save();
+			}
+		} else {
+			$baseElement->path = $newelement->path;
+			$baseElement->label = $newelement->label;
+			$baseElement->id = 0;
+			$baseElement->save();
+		}
+	}
+
+	//Déplacement d'un répertoire ou d'un fichier en base et en physique
+	public static function move($path,$to, $label=''){
+		global $conf, $myUser;
+
+		$osPath = File::convert_decoding($path);
+		$osTo = File::convert_decoding($to);
+
+		if(!file_exists($osPath)) return;
+		
+		$element = Element::fromPath($path);
+		
+		
+		//Si le dossier se termine par un ., on supprime ce . (non pris en charge par l'os)
+		if($element->type=="directory" && substr($to, -1,1) =='.')
+			$to = substr($to, 0,strlen($to)-1);
+		
+		
+		if($to == $path) return $element;
+
+		if(!self::hasRight($element,'edit')) throw new Exception("Permissions insuffisantes",403);
+
+		$parentPathTo = dirname($to);
+		$parentPathOsTo = dirname($osTo);
+		if(!file_exists($parentPathOsTo))  throw new Exception("Dossier de destination inexistant",404);
+		$parentTo = Element::fromPath($parentPathTo);
+		if(!self::hasRight($parentTo,'read')) throw new Exception("Permissions insuffisantes",403);
+	
+
+		$dbPath = str_replace('\\','/',$element->path);
+			
+			if(file_exists($osTo)) return false;
+
+			if($element->type == 'directory'){
+				
+				$element->path = $oldPath = $element->path;
+				
+				//Si c'est un dossier, on récupere tous les élements commencant par ce nom afin de modifier leurs chemins
+				$baseElement = Element::staticQuery('SELECT * FROM {{table}} WHERE `path` = ? OR `path` LIKE ?',array($dbPath,$dbPath.'/%'), true);
+			} else {
+				$baseElement = Element::load(array('path'=>$dbPath));
+				if(is_object($baseElement) && $baseElement->id != 0) $element->id = $baseElement->id;
+			}
+			try {
+				if(!rename($osPath,$osTo))
+					throw new Exception('Un élément du même nom existe déjà.');
+			} catch (Exception $e) {
+				return false;
+			}
+			$toElement = Element::fromPath($to);
+			//Si type est image, on supprime l'ancienne miniature
+			if(in_array($element->extension, array('jpg','png','jpeg','gif','bmp')) && file_exists(self::root().'.thumbnails'.SLASH.base64_encode($element->path).'.'.$element->extension))
+				unlink(self::root().'.thumbnails'.SLASH.base64_encode($element->path).'.'.$element->extension);
+
+			if(is_array($baseElement)){
+				foreach ($baseElement as $elem) {
+					if($elem->path == $oldPath)
+						$elem->label = $toElement->label;
+
+					$elem->path = str_replace($dbPath, $toElement->path, $elem->path);
+					$elem->save();
+				}
+			} else {
+				$baseElement->path = $toElement->path;
+				$baseElement->label = $toElement->label;
+				$baseElement->save();
+			}
+		
+
+		return $toElement;
+	}
+
+	//Ajout d'un fichier en base et en physique (prend de l'utf8 en entré pour le path)
+	public static function addFile($path,$stream){
+		//on enregistre le fichier dans l'encodage du système
+		$osPath = File::convert_decoding($path);
+		$parentPath = dirname($path);
+		$parentOsPath = File::convert_decoding($parentPath);
+		if(!file_exists($parentOsPath))  throw new Exception("Dossier de destination inexistant",404);
+		$parent = Element::fromPath($parentPath);
+		if(!self::hasRight($parent,'read')) throw new Exception("Permissions insuffisantes",403);
+
+		file_put_contents($osPath, $stream);
+
+		$element = Element::fromPath($path, array('preview'=>false));
+		
+		$element->save();
+		return $element;
+	}
+
+	//Ajout d'un dossier en base et en physique  (prend de l'utf8 en entré pour le path)
+	public static function addFolder($path, $recursive = false, $checkRight=true, $creator = null){
+		$osPath = File::convert_decoding($path);
+		$parentPath = dirname($path);
+		$osParentPath = dirname($osPath);
+		if(!file_exists($osParentPath)) {
+			if(!$recursive){
+				throw new Exception("Dossier parent ".$parentPath."  inexistant",404);
+			} else {
+				self::addFolder($parentPath, $recursive, $checkRight, $creator);
+			}
+		}
+		$parent = Element::fromPath($parentPath);
+
+		if($checkRight && !self::hasRight($parent,'read')) throw new Exception("Permissions insuffisantes",403);
+
+		if(file_exists($osPath)) return;
+		mkdir($osPath,0755,$recursive);
+		$element = Element::fromPath($path);
+		if(isset($creator)) $element->creator = $creator;
+		$element->save();
+		return $element;
+	}
+
+	public static function hasRight($element,$type){
+		global $myUser,$myFirm;
+		require_once(__DIR__.SLASH.'ElementRight.class.php');
+		$documentRight = '';
+		switch($type){
+			case 'read':
+				$documentRight = 'read';
+			break;
+			case 'delete':
+				$documentRight = 'edit';
+			break;
+			case 'edit':
+				$documentRight = 'edit';
+			break;
+		}
+
+		if(!$myUser->can('document',$type)) return false;
+		if($myUser->login == $element->creator || $myUser->can('document','configure') || $myUser->superadmin == 1) return true;
+
+		$allPathes = array();
+		$rootPath = '';
+		foreach (explode(SLASH,$element->path) as $i=>$crumb) {
+			$rootPath .= ($i==0?'':SLASH).$crumb;
+			$allPathes[] = str_replace("\\","\\\\",$rootPath);
+		}
+
+		$userRanks = array();
+		foreach ($myUser->ranks[$myFirm->id] as $rank) 
+			$userRanks[] = $rank->id;
+		
+		$query = 'SELECT COUNT(id) allowed FROM {{table}} WHERE (element IN(SELECT id from '.Element::tableName().' WHERE path IN("'.implode('","',$allPathes).'")) AND `'.$documentRight.'`=1) AND (entity="all" OR (entity="user" AND uid=?) ';
+		
+		if(count($userRanks)!=0) $query .= ' OR (entity="rank" AND uid IN('.implode(',',$userRanks).')) ';
+		
+		$query .= ')';
+		
+		$result = ElementRight::staticQuery($query,array($myUser->login));
+		$result = $result->fetch();
+		if(!$result || $result['allowed']<=0) return false;
+		return true;
+	}
+
+	public static function remove_dir_recursive($path) {
+		foreach(glob($path.SLASH.'*') as $element){
+			if(mt_basename($element) == '.' || mt_basename($element) == '..') continue;
+			if(is_dir($element)) {
+				self::remove_dir_recursive($element);
+			} else {
+				unlink($element);
+			}
+		}
+		rmdir($path);
+	}
+
+	
+
+}
+?>

+ 26 - 0
plugin/document/ElementRight.class.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * Define a elementright.
+ * @author Valentin CARRUESCO
+ * @category Plugin
+ * @license copyright
+ */
+class ElementRight extends Entity{
+	public $id,$element,$entity,$uid,$read,$edit,$recursive;
+	protected $TABLE_NAME = 'document_right';
+	protected $links = array(
+		'element' => 'Element'
+	);
+	public $fields =
+	array(
+		'id' => 'key',
+		'element' => 'int',
+		'entity' => 'string',
+		'uid' => 'string',
+		'read' => 'int',
+		'edit' => 'int',
+		'recursive' => 'int'
+	);
+	public $indexes = array('element');
+}
+?>

+ 676 - 0
plugin/document/WebDav.class.php

@@ -0,0 +1,676 @@
+<?php
+
+class WebDav{
+	public $root,$folder,$logs,$version = 1,$user,$lockfile,$ignoreFiles = array();
+	public $on = array(
+		'login' => null,
+		'log' => null,
+		'edit' => null,
+		'move' => null,
+		'delete' => null,
+		'list' => null,
+		'properties' => null,
+		'copy' => null
+	);
+	public $do = array(
+		'edit' => null,
+		'move' => null,
+		'delete' => null,
+		'list' => null,
+		'copy' => null
+	);
+
+	public static function logPath() {
+		return __DIR__.SLASH.'logs';
+	}
+
+	function start(){
+
+		$server = $_SERVER;
+		$method = strtoupper($server['REQUEST_METHOD']);
+		$headers = getallheaders();
+		$body = file_get_contents("php://input");
+
+		$requestUri = explode('/',$server['REQUEST_URI']);
+		$requestUri = array_filter($requestUri);
+		$requestUri = '/'.implode('/',$requestUri);
+		$server['REQUEST_URI'] =  $requestUri;
+
+		$log = PHP_EOL.PHP_EOL.'---------- '.$method.' --------------';
+		$log .= PHP_EOL.'Fichier : '.$requestUri;
+		foreach($headers as $key => $header){
+			$log .=  PHP_EOL."\t - ".$key.' : '.$header;
+		}
+		$log .= PHP_EOL."\t === ".'Requête : '.PHP_EOL. $body;
+
+		$this->logit($log);
+
+	
+		
+
+		if(!file_exists(self::logPath()))
+			mkdir(self::logPath(),0755,true);
+
+		if($method!='OPTIONS' && isset($this->do['login']) ){
+			try{
+				$function = $this->do['login'];
+
+				$user = isset($server['PHP_AUTH_USER']) ? $server['PHP_AUTH_USER']: '';
+				$password = isset($server['PHP_AUTH_PW']) ? $server['PHP_AUTH_PW']: '';
+
+				$header = apache_request_headers(); 
+			
+				if(isset($header['Authorization']) && $header['Authorization']!=''){
+					$authorization = explode(' ',$header['Authorization']);
+					if(count($authorization)==2){
+						$credentials = explode(':',base64_decode($authorization[1]));
+						if(isset($credentials[0]))  $user = $credentials[0];
+						if(isset($credentials[1]))  $password = $credentials[1];
+					}
+				}
+
+				$user = $user =='' ? 'anonymous': $user;
+				$password = $password =='' ? 'anonymous': $password;
+
+				$this->user = $function($user,$password);
+				
+			}catch(Exception $e){		    
+		    	$this->logit('REQUEST URI : '.$requestUri.PHP_EOL.'REQUEST HEADERS : '.json_encode($headers));
+				$this->logit('Error '.self::headerFromCode(401).'  for -'. $user.' :'.$e->getCode().' '.$e->getMessage() );
+				$this->logit(json_encode($_SERVER,JSON_PRETTY_PRINT));
+				$header = apache_request_headers(); 
+				$header = $this->logit(json_encode($header,JSON_PRETTY_PRINT));
+				$header = $this->logit(json_encode($_REQUEST,JSON_PRETTY_PRINT));
+					
+				self::emit_header($e->getCode());
+	    		if($e->getCode()==401)
+					header('WWW-Authenticate: Basic realm="Software Webdav gate"');
+			    
+		    	echo $e->getMessage();
+		    	exit();
+		    }
+		}
+
+		
+
+		try{
+
+	        
+			switch($method) {
+
+				//Returns a list of properties of a file, a directory of a directory and its files.
+				case 'PROPFIND':
+					$log= "\t === ".'Réponse : '.PHP_EOL;
+					try{
+						$depth = isset($headers['Depth']) ? $headers['Depth'] : 0;
+
+						if(in_array(mt_basename($server['REQUEST_URI']), $this->ignoreFiles)){
+							$this->logit('File in blacklist ignored'.PHP_EOL);
+							return self::emit_header(404);
+						}
+
+						$scanned = $this->folder.$this->get_path($server);
+
+						$files = array();
+						
+						if(isset($this->do['list']) ){
+							$function = $this->do['list'];
+							$files = $function($scanned,$depth);
+						}
+						
+						$locks = $this->lock();
+						$xml = '';
+						
+						foreach ($files as $i=>$file) {
+							$url = $requestUri.'/';
+							//Si on est pas sur le premier dossier/fichier d'origine, c'est qu'on est sur un enfant du dossier, on concatene son url au dossier
+							if($i!=0) $url .= rawurlencode($file['label']);
+
+							$xml .= self::xml_file($url,$file,$locks);
+						}
+
+						$stream = '<D:multistatus xmlns:D="DAV:">'.$xml.'</D:multistatus>';
+
+						self::emit_header(207);
+						header('DAV: '.$this->version);
+						header('Content-Type: text/xml');
+						echo $stream;
+
+						$log.= "\t".'DAV: '.$this->version.PHP_EOL;
+						$log.= "\t\t".'Content-Type: text/xml'.PHP_EOL;
+						$log.= "\t\t".$stream.PHP_EOL;
+						$this->logit($log);
+
+					}catch(Exception $e){
+
+						$log.= "\t".$e->getCode().' : '.$e->getMessage().PHP_EOL;
+						$this->logit($log);
+				    	self::emit_header($e->getCode());
+			    	}
+		      	break;
+
+		      	//Récuperation d'un fichier (ou dossier ?)
+		      	case 'GET':
+		      		$log= "\t === ".'Réponse : '.PHP_EOL;
+			      	try{
+				        $file = $this->folder.$this->get_path($server);
+
+				      	$this->logit('Download '.$file);
+						if(!isset($this->do['download']) ) throw new Exception("Not implemented", 501);
+						
+						$function = $this->do['download'];
+						$stream = $function($file);
+						self::emit_header(200);
+						header('Content-Length: '.strlen($stream));
+						echo $stream;
+
+						$log.= "\t".'- 200 OK'.PHP_EOL;
+						$log.= "\t".'- Content-Length: '.strlen($stream);
+						$log.= "\t\t".$stream.PHP_EOL;
+						$this->logit($log);
+			        }catch(Exception $e){
+			        	$log.= "\t".'- '.$e->getCode().' '.$e->getMessage().PHP_EOL;
+						$this->logit($log);
+				    	self::emit_header($e->getCode());
+				    	header('Content-Type: text/xml');
+				    }
+			    break;
+
+			    //Envoi d'un fichier
+			    case 'PUT':
+			    	$log= "\t === ".'Réponse : '.PHP_EOL;
+			      	try{
+				        $target = $this->folder.$this->get_path($server);
+				        
+
+						$this->logit('PUT '.$target);
+						if(!isset($this->do['edit']) ) throw new Exception("Not implemented", 501);
+						
+						$function = $this->do['edit'];
+						$function($target, $body,'file');
+						self::emit_header(201);
+						
+
+						$log.= "\t".'- 201 Created'.PHP_EOL;
+						$this->logit($log);
+
+				    }catch(Exception $e){
+				   
+				    	$log.= "\t".'- '.$e->getCode().' '.$e->getMessage().PHP_EOL;
+						$this->logit($log);
+				    	self::emit_header($e->getCode());
+				    }
+			    break;
+
+			    //Suppression dossier/fichier
+			    case 'DELETE':
+			    	$log= "\t === ".'Réponse : '.PHP_EOL;
+			      	try{
+				        $path = $this->folder.$this->get_path($server);
+				        if(!isset($this->do['delete']) ) throw new Exception("Error Processing Request", 501);
+						$function = $this->do['delete'];
+						$function($path);
+						self::emit_header(200);
+
+					}catch(Exception $e){
+				    	$log.= "\t".'- '.$e->getCode().' '.$e->getMessage().PHP_EOL;
+						$this->logit($log);
+				    	self::emit_header($e->getCode());
+				    }
+
+			    break;
+
+			    //Déplacement/renommage dossier/fichier
+			    case 'MOVE':
+			    	$log= "\t === ".'Réponse : '.PHP_EOL;
+			      	try{
+				      	
+
+				        $target = $this->folder.$this->get_path($server);
+
+				        $toUrl = rawurldecode($server['HTTP_DESTINATION']);
+				        $toUrl = parse_url($toUrl, PHP_URL_PATH);
+
+				        $this->logit('Move to '.$toUrl);
+
+				        $to = $this->folder.str_replace('/',SLASH,utf8_decode(str_replace($this->root,'',$toUrl)));
+				        $to = substr($to, -1,1) == SLASH ? substr($to, 0,strlen($to)-1): $to;
+
+				        $this->logit('Move '.$target.' to '.$to.' (url :'.$toUrl.')');
+				        
+				        if(!isset($this->do['move']) ) throw new Exception("Not implemented", 501);
+				        
+						$function = $this->do['move'];
+						$function($target,$to);
+						self::emit_header(200);
+		
+			        }catch(Exception $e){
+				    	$log.= "\t".'- '.$e->getCode().' '.$e->getMessage().PHP_EOL;
+						$this->logit($log);
+				    	self::emit_header($e->getCode());
+				    }
+			    break;
+
+		        //Creation de dossier
+		        case 'MKCOL':
+		        	$log= "\t === ".'Réponse : '.PHP_EOL;
+			      	try{
+				      	
+				        $path = $this->folder.$this->get_path($server);
+				        $this->logit('Create folder '.$path);
+
+						if(isset($this->do['edit']) ){
+							$function = $this->do['edit'];
+							$function($path, '','create','directory');
+							self::emit_header(200);
+							break;
+						}
+						self::emit_header(200);
+				        mkdir($path,0755,true);
+			        }catch(Exception $e){
+				    	$log.= "\t".'- '.$e->getCode().' '.$e->getMessage().PHP_EOL;
+						$this->logit($log);
+				    	self::emit_header($e->getCode());
+				    }
+		    	break;
+
+		    	//The OPTIONS method allows an http client to find out what HTTP methods are supported on a specific url.
+		    	case 'OPTIONS':
+		    		$log= "\t === ".'Réponse : '.PHP_EOL;
+		        //header('Allows: options get head post delete trace propfind proppatch copy mkcol put lock unlock');
+		        	if (ob_get_length()) ob_end_clean();
+		        	header('Allows: options get head delete propfind copy mkcol put lock unlock proppatch');
+		        	$log.= "\t".'- Allows: options get head delete propfind copy mkcol put lock unlock proppatch'.PHP_EOL;
+					$this->logit($log);
+		      	break;
+
+		      	case 'HEAD':
+		      		$log= "\t === ".'Réponse : '.PHP_EOL;
+		      		self::emit_header(200);
+		      		$log.= "\t".'- 200 Ok'.PHP_EOL;
+					$this->logit($log);
+		      	break;
+
+		      	case 'POST':
+		      		$log= "\t === ".'Réponse : '.PHP_EOL;		      	
+		        	self::emit_header(501);
+		        	$log.= "\t".'- 501 Not implemented'.PHP_EOL;
+					$this->logit($log);
+		      	break;
+
+		      	case 'TRACE':
+		      		$log= "\t === ".'Réponse : '.PHP_EOL;		      	
+		        	self::emit_header(501);
+		        	$log.= "\t".'- 501 Not implemented'.PHP_EOL;
+					$this->logit($log);
+		      	break;
+
+		      	//Updates properties of a resource or collection.
+		      	case 'PROPPATCH':
+		      		$log= "\t === ".'Réponse : '.PHP_EOL;
+					$properties = array();
+					$namespaces = array();
+				   	$ns = '';
+				   	preg_match_all('|xmlns:([^\=]*)="([^"]*)"|iUs', $body, $matches,PREG_SET_ORDER);
+				   	foreach ($matches as $match) {
+				   		if(count($match)!=3) continue;
+				   		$namespaces[$match[1]] = $match[2];
+
+				   		$ns.=' xmlns:'.$match[1].' = "'.$match[2].'" ';
+				   	}
+
+				   	preg_match('|<D:prop>(.*)<\/D:prop>|iUs', $body, $matches);
+				   	if(isset($matches[1])){
+					   	preg_match_all('|<([^:]*):([^\>]*)>([^\<]*)<\/\1:\2>|iUs', $matches[1], $matches,PREG_SET_ORDER);
+					   	foreach ($matches as $match) {
+					   		if(count($match)==4)
+					   			$properties[$match[2]] = array('ns'=>$match[1],'value'=>$match[3],'key'=>$match[2]);
+					   	}
+				   	}
+
+				   	$this->logit('Set properties '.json_encode($properties,JSON_PRETTY_PRINT));
+					if(isset($this->on['properties']) ){
+						self::emit_header(200);
+						$function = $this->do['properties'];
+						$function($properties,$namespaces);
+						
+						break;
+					}
+
+				   	$response = '<?xml version="1.0" encoding="utf-8"?>
+					<d:multistatus xmlns:d="DAV:" '.$ns.'>
+					  <d:response>
+					    <d:href>/</d:href>
+					    <d:propstat>
+					      <d:status>HTTP/1.1 200 OK</d:status>
+					      <d:prop>
+					      ';
+					        foreach ($properties as $key => $value) {
+					        	$response .= '<'.$value['ns'].':'.$value['key'].' />';
+					        }
+					$response .= '
+					       
+					      </d:prop>
+					    </d:propstat>
+					  </d:response>
+					</d:multistatus>';
+
+				   	$this->logit('[RESPONSE]'.PHP_EOL.$response);
+			       	self::emit_header(207);
+			       	header('Content-Type: text/xml');
+			       	echo $response;
+			       	$log.= "\t".'- 207 Multi status'.PHP_EOL;
+			       	$log.= "\t".'- Content-Type: text/xml'.PHP_EOL;
+			       	$log.= "\t\t".$response.PHP_EOL;
+					$this->logit($log);
+			    break;
+
+		    	//Copie d'un élement vers un nouvel emplacement
+		    	case 'COPY':
+		    		$log= "\t === ".'Réponse : '.PHP_EOL;
+			      	try{
+				        $target = $this->folder.$this->get_path($server);
+
+				        $toUrl = rawurldecode($server['HTTP_DESTINATION']);
+				        $toUrl = parse_url($toUrl, PHP_URL_PATH);
+				        $to = $this->folder.str_replace('/',SLASH,utf8_decode(str_replace($this->root,'',$toUrl)));
+				        $this->logit('Copy '.$target.' to '.$to.' (url :'.$toUrl.')');
+				        
+				        if(!isset($this->do['copy'])) throw new Exception("Not implemented", 501);
+				        
+						$function = $this->do['copy'];
+						$function($target,$to);
+						self::emit_header(200);
+
+						$log.= "\t".'- 200 OK'.PHP_EOL;
+						$this->logit($log);
+					
+			        }catch(Exception $e){
+				    	$log.= "\t".'- '.$e->getCode().' '.$e->getMessage().PHP_EOL;
+						$this->logit($log);
+				    	self::emit_header($e->getCode());
+				    }
+		    	break;
+
+		    	//Verouillage d'un élement
+		    	case 'LOCK':
+		    		$log= "\t === ".'Réponse : '.PHP_EOL;
+		    		try{
+			    		$target = $this->folder.$this->get_path($server);
+						$response = 200;
+						$locks = $this->lock();
+
+						//Renouvellement de lock ou demande par un autre user
+						if(isset($locks[base64_encode($requestUri)])){
+							$lock = $locks[base64_encode($requestUri)];
+
+							if($lock['owner']!=$this->user->login) {
+								$response = 423;
+							}else{
+								$properties = array(
+									'owner'=>$this->user->login,
+									'created'=>time(),
+									'depth'=>isset($headers['Depth']) ? $headers['Depth'] : 'Infinity',
+									'timeout'=>isset($headers['Timeout']) ? $headers['Timeout'] : 'Second-3600',
+								);
+								$lock = $this->lock($requestUri,$properties);
+							}
+						//Création d'un nouveau lock
+						}else{
+							$properties = array(
+								'owner'=>$this->user->login,
+								'created'=>time(),
+								'depth'=>isset($headers['Depth']) ? $headers['Depth'] : 'Infinity',
+								'timeout'=>isset($headers['Timeout']) ? $headers['Timeout'] : 'Second-3600',
+							);
+							$lock = $this->lock($requestUri,$properties);
+						}
+
+				        self::emit_header($response);
+				        header('Lock-Token: <'.$lock['token'].'>');
+
+				        $requestUri = str_replace('&','%26',$requestUri);
+				        $xml = '<?xml version="1.0" encoding="utf-8" ?>
+						   <D:prop xmlns:D="DAV:">
+						     <D:lockdiscovery>
+						       <D:activelock>
+						         <D:locktype><D:write/></D:locktype>
+						         <D:lockscope><D:exclusive/></D:lockscope>
+						         <D:depth>'.$lock['depth'].'</D:depth>
+						         <D:owner>
+						           <D:href>'.$lock['owner'].'</D:href>
+						         </D:owner>
+						         <D:timeout>'.$lock['timeout'].'</D:timeout>
+						         <D:locktoken>
+						           <D:href>'.$lock['token'].'</D:href>
+						         </D:locktoken>
+						         <D:lockroot>
+						           <D:href>'.$requestUri.'</D:href>
+						         </D:lockroot>
+						       </D:activelock>
+						     </D:lockdiscovery>
+						   </D:prop>';
+				        echo $xml;
+
+				        $log.= "\t".'- '.$response.' '.self::headerFromCode($response).PHP_EOL;
+				        $log.= "\t".'-  Lock-Token: <'.$lock['token'].'>'.PHP_EOL;
+				        $log.= "\t\t".$xml.PHP_EOL;
+						$this->logit($log);
+
+				    }catch(Exception $e){
+				    	$log.= "\t".'- '.$e->getCode().' '.$e->getMessage().PHP_EOL;
+						$this->logit($log);
+				    	self::emit_header($e->getCode());
+				    }
+		    	break;
+
+		    	//Déverouillage d'un élement
+		    	case 'UNLOCK':
+		    		$log= "\t === ".'Réponse : '.PHP_EOL;
+		    		try{
+			    		$target = $this->folder.$this->get_path($server);
+			    		$token = isset($headers['Lock-Token']) ? $headers['Lock-Token'] : 'no-token';
+			    		$token = preg_replace('|[\<\>]|is','',$token);
+			    		$locks = $this->lock();
+
+			    		if(isset($locks[base64_encode($requestUri)])){
+			    			if($locks[base64_encode($requestUri)]['token'] == $token){
+					    		$this->lock($requestUri,false);
+			    			}
+			    		}
+			    		self::emit_header(204);
+			    		$log.= "\t".'- 204 No Content'.PHP_EOL;
+						$this->logit($log);
+		    		}catch(Exception $e){
+				    	$log.= "\t".'- '.$e->getCode().' '.$e->getMessage().PHP_EOL;
+						$this->logit($log);
+				    	self::emit_header($e->getCode());
+				    }
+		    	break;
+		    }
+		}catch(Exception $e){
+			self::emit_header(500);
+		}
+	    exit();
+	}
+
+	//Méthode de gestion du manifest des locks fichiers (ajout, supression, listing)
+	// Si properties est a false : supression d'un verrou pour 'file'
+	// Si file + properties est renseigné : Création d'un verrou pour 'file' avec ces propriétés
+	// Si file et properties sont a vide : on retourne juste la liste des verrou non expirés
+	public function lock($file=null,$properties = array()){
+		if(!file_exists($this->lockfile)) file_put_contents($this->lockfile,json_encode(array()));
+		$locks = json_decode(file_get_contents($this->lockfile),true);
+
+		foreach($locks as $key=>$lock){
+			$timeoutInfos = explode('-',$lock['timeout']);
+
+      		//Calcul du délais d'expiration d'un verrou
+      		$secondNumber = 60;
+      		if(count($timeoutInfos)==2){
+      			list($unity,$number) = $timeoutInfos;
+      			$secondNumber = $number;
+      			if($unity == 'Minut') $secondNumber *= 60;
+      			if($unity == 'Hour') $secondNumber *= 3600;
+      			if($unity == 'Day') $secondNumber *= 86400;
+      		}
+
+      		//Supression du verrou fichier si expiré
+  			if(time() <= ($secondNumber + $lock['created'])) continue;
+			unset($locks[$key]);
+		}
+		//Si aucun parametre n'est spécifié, on retourne la liste des verrous
+		if($file==null) return $locks;
+		
+		//Si les properties sont a false, on supprime le verrou
+		if($properties===false){
+			unset($locks[base64_encode($file)]);
+		}else{
+			//Si les properties du verrou sont renseigné, on le save
+			$properties['token'] = isset($properties['token']) && $properties['token'] != '' ? $properties['token'] : 'fr.sys1:'.sha1($file);
+			$locks[base64_encode($file)] = $properties;
+		}
+
+		file_put_contents($this->lockfile, json_encode($locks));
+
+		return !$properties ? '': $locks[base64_encode($file)];
+	}
+
+	/*
+
+		create_time : timestamp de création
+		update_time : timestamp dernière modification
+		length : taille du dossier ou du fichier
+		type : directory/file
+
+	*/
+	public static function xml_file($url,$fileInfos,$locks){
+		
+
+		if(!isset($fileInfos['create_time'])) $fileInfos['create_time'] =  time();
+		if(!isset($fileInfos['update_time'])) $fileInfos['update_time']  =  time();
+		if(!isset($fileInfos['length'])) $fileInfos['length'] = 0;
+	
+		
+		$fileInfos['create_time'] = $fileInfos['create_time'] - (3600*2);
+		$fileInfos['update_time'] = $fileInfos['update_time'] - (3600*2);
+
+		$fileInfos['update_time'] = date('D, j M Y H:i:s',  $fileInfos['update_time']).' GMT';
+		$fileInfos['create_time'] = date('D, j M Y H:i:s',  $fileInfos['create_time']).' GMT';
+
+		$url = str_replace('&','%26',$url);
+
+
+		$xml = '<D:response>
+					<D:href>'.$url.'</D:href>';
+
+	    $xml .= '<D:propstat>
+	              <D:prop>';
+	            if($fileInfos['type']=='directory'){
+	            $xml .='<D:resourcetype>
+	                  <D:collection/>
+	                </D:resourcetype>';
+	            }else{
+	            	$xml .='<D:resourcetype/>';
+	            	$xml .='<D:supportedlock>
+                    <D:lockentry>
+			            <D:lockscope><D:exclusive/></D:lockscope>
+			            <D:locktype><D:write/></D:locktype>
+			          </D:lockentry>
+                  </D:supportedlock>';
+	            }
+
+	            if(isset($locks[base64_encode($url)])){
+	              		$lock = $locks[base64_encode($url)];
+	      				//Envoi du verrou au client si un verrou est en place
+	              		$xml .='<D:lockdiscovery>
+                        	<D:activelock>
+                        	    <D:locktype><D:write/></D:locktype>
+                        	    <D:lockscope><D:exclusive/></D:lockscope>
+                        	    <D:depth>'.$lock['depth'].'</D:depth>
+                        	    <D:owner>'.$lock['owner'].'</D:owner>
+                        	    <D:timeout>'.$lock['timeout'].'</D:timeout>
+                        	    <D:locktoken>
+                        	        <D:href>'.$lock['token'].'</D:href>
+                        	    </D:locktoken>
+                        	</D:activelock>
+	                    </D:lockdiscovery>';
+	            }
+	             
+
+              	if(isset($fileInfos['length']))
+	              	 $xml.= '<D:getcontentlength>'.$fileInfos['length'].'</D:getcontentlength>';
+	           $xml .='
+	                <D:getetag>"'.sha1($fileInfos['update_time']).'"</D:getetag>
+	                <D:getlastmodified>'.$fileInfos['update_time'].'</D:getlastmodified>
+	                <D:creationdate>'.$fileInfos['create_time'].'</D:creationdate>
+	              </D:prop>
+	              <D:status>HTTP/1.1 200 OK</D:status>
+	           </D:propstat>';
+	           
+	           if(!isset($fileInfos['length'])){
+	           		$xml .=  '<D:propstat>
+	              <D:prop>
+	                <D:getcontentlength/>
+	              </D:prop>
+	              <D:status>HTTP/1.1 404 Not Found</D:status>
+	           		</D:propstat>';
+	           }
+	 		
+	    $xml .= '</D:response>';
+	    return $xml;
+	}
+
+	function get_path($server){
+		$path = $this->get_parameter($server['REQUEST_URI']);
+		$path = rawurldecode(utf8_decode($path));
+		return str_replace('/',SLASH,$path);
+	}
+
+	public function get_parameter($parameter){
+		$path = rawurldecode($parameter);
+		$path = str_replace('\\', '/', $path);
+		$path = str_replace('//', '/', $path);
+		$path = substr($path,strlen($this->root));
+		
+		if(substr($path, -1)=='/') $path = substr($path, 0,-1);
+		return $path;
+	}
+
+
+	function logit($message){
+		if(isset($this->on['log']) ){
+			$function = $this->on['log'];
+			$function($message);
+		}
+
+		if($this->logs=='') return;
+		file_put_contents($this->logs, $message.PHP_EOL,FILE_APPEND);
+	}
+
+	public static function emit_header($code){
+		if (ob_get_length()) ob_end_clean();
+		header(self::headerFromCode($code));
+	}
+
+	public static function headerFromCode($code){
+		$codes = array(
+			200 => 'HTTP/1.1 200 Ok',
+	        201 => 'HTTP/1.1 201 Created',
+	        204 => 'HTTP/1.1 204 No Content',
+	        207 => 'HTTP/1.1 207 Multi-Status',
+	        401 => 'HTTP/1.0 401 Unauthorized',
+	        403 => 'HTTP/1.1 403 Forbidden',
+	        404 => 'HTTP/1.1 404 Not Found',
+	        406 => 'HTTP/1.1 406 Not acceptable',
+	        409 => 'HTTP/1.1 409 Conflict',
+	        415 => 'HTTP/1.1 415 Unsupported Media Type',
+	        423 => 'HTTP/1.1 423 Locked',
+	        500 => 'HTTP/1.1 500 Internal Server Error',
+	        501 => 'HTTP/1.1 501 Method not implemented',
+		);
+		return isset($codes[$code]) ? $codes[$code] : $codes[500];
+	}
+}
+
+?>

+ 551 - 0
plugin/document/action.php

@@ -0,0 +1,551 @@
+<?php
+global $_,$conf;
+switch($_['action']){
+
+	case 'document_load_template':
+		
+		global $myUser,$_;
+		require_once(__DIR__.SLASH.'template.document.php');
+		
+	break;
+
+	case 'document_widget_load':
+		global $myUser;
+		require_once(PLUGIN_PATH.'dashboard'.SLASH.'DashboardWidget.class.php');
+		$widget = DashboardWidget::current();
+		
+		$root = $widget->data('widget-document-root');
+		$root = !empty($root) ? ': <strong>'.$root.')</strong>':'';
+
+		$widget->title = 'Mes documents'.$root;
+		ob_start();
+		require_once(__DIR__.SLASH.'widget.php');
+		$widget->content = ob_get_clean();
+		echo json_encode($widget);
+	break;
+
+	case 'document_widget_configure_save':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			require_once(PLUGIN_PATH.'dashboard'.SLASH.'DashboardWidget.class.php');
+
+			User::check_access('document','configure');
+
+			$widget = DashboardWidget::getById($_['id']);
+			
+			$widget->data('widget-document-tree',$_['widget-document-tree']);
+			$widget->data('widget-document-detail',$_['widget-document-detail']);
+			$widget->data('widget-document-search',$_['widget-document-search']);
+
+			$root = str_replace(array('./','../'),'',$_['widget-document-root']);
+			$widget->data('widget-document-root',$root);
+			
+			$widget->save();
+		});
+	break;
+
+	case 'document_widget_configure':
+		global $myUser;
+		require_once(PLUGIN_PATH.'dashboard'.SLASH.'DashboardWidget.class.php');
+
+		$widget = DashboardWidget::current();
+		ob_start();
+		require_once(__DIR__.SLASH.'widget.configure.php');
+		$content = ob_get_clean();
+		echo $content ;
+	break;
+
+
+	
+
+	case 'document_embedded':
+		Action::write(function(&$response){
+			Plugin::addCss("/css/main.css"); 
+			Plugin::addJs("/js/main.js"); 
+
+			ob_start();
+			global $myUser,$_;
+			$embedded = true;
+			//l'ui de la ged prend en entrée / quel que soit l'os
+			if(isset($_['data']['root'])) $_['data']['root'] = str_replace('\\', '/', $_['data']['root']);
+			require_once(__DIR__.SLASH.'page.list.php');
+			$response['html'] = ob_get_clean();
+		});
+	break;
+
+
+	case 'document_folder_create':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('document','edit');
+			require_once(__DIR__.SLASH.'Element.class.php');
+			$path = str_replace('/',SLASH,$_['path']);
+			$path =  Element::root().$path;
+			if(!document_check_element_name(htmlspecialchars_decode(html_entity_decode($_['folder']), ENT_QUOTES))) throw new Exception("Caractères interdits : \\/:*?\"<>|");
+
+			if(strlen($_['folder']) > 80) throw new Exception("Taille maximale autorisée de 80 caractères.");
+			Element::addFolder($path);
+			if($conf->get('document_enable_logs'))  Log::put("Création d'un dossier : ".$path,'document');
+
+		});
+	break;
+
+	
+
+	/** ELEMENT **/	
+
+
+	//Récuperation d'une liste de element
+	case 'document_element_search':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('document','read');
+			require_once(__DIR__.SLASH.'Element.class.php');
+			
+			
+			//recherche par libellé
+			if(!empty($_['keyword'])){
+				$query = 'SELECT * FROM {{table}} WHERE 1';
+				$data = $elements = array();
+				
+				$query .= ' AND label LIKE ?';
+				$data[] = '%'.$_['keyword'].'%';				
+
+				$folder = isset($_['folder']) && !empty($_['folder']) ? $_['folder'] : '.';
+				if(isset($_['folder']) && !empty($_['folder']))
+					$query .= ' AND `path` LIKE "'.$_['folder'].'%'.'"';
+
+				
+				//Tri des colonnes
+				if(isset($_['sort'])) sort_secure_query($_['sort'],array('label','creator','size'),$query,$data);
+
+				$response['qry'] = $query;
+				foreach (Element::staticQuery($query,$data,true) as $element) {
+					// Check pour ne pas faire ressortir le dossier lui même
+					if ($element->path == $folder) continue;
+					$elemPath =str_replace(SLASH.'.'.SLASH,SLASH,Element::root().$element->path) ;
+					$line = Element::fromPath($elemPath);
+					$line->path = rtrim($line->path, SLASH);
+
+					$osPath = Element::root().str_replace('/',SLASH,$line->path);
+					if(!file_exists($osPath)){
+						Element::deleteById($line->id);
+						continue;
+					}
+
+					$row = $line->toArray();
+					
+					$row['updatedRelative'] = relative_time($line->updated);
+					$row['sizeReadable'] = $row['type'] == 'directory' ? $line->childNumber.' élements' :  readable_size($line->size);
+					$row['updatedReadable'] = day_name(date('N',$line->updated)).' '. date('d ',$line->updated).month_name(date('m',$line->updated)).date(' Y à H:i',$line->updated);
+					$row['thumbnail'] = $line->thumbnail();
+					$row['icon'] = $line->icon();
+					$row['childNumber'] = $line->childNumber;
+					
+
+					$elements[] = $row;
+				}
+
+				$response['rows'] = $elements;
+			//recherche par arborescence
+			}else{
+				if(isset($_['folder']) && !empty($_['folder'])){
+				$folder = str_replace('/',SLASH,$_['folder']);
+				}else {
+					$folder = '.';
+					if(isset($_['root'])) $folder = str_replace('/',SLASH,$_['root']);
+				}
+
+				$response['rows'] = array();
+				$scanned = Element::root().$folder.SLASH.'*';
+				
+				//L'ui ne traite que les / quel que soit l'os
+				foreach (Element::browse($scanned) as $line) {
+					$line->path = str_replace('\\', '/', $line->path);
+					$row = $line->toArray();
+
+					$row['updatedRelative'] = relative_time($line->updated);
+					$row['sizeReadable'] = $row['type'] == 'directory' ? $line->childNumber.' élements' :  readable_size($line->size);
+					$row['updatedReadable'] = day_name(date('N',$line->updated)).' '. date('d ',$line->updated).month_name(date('m',$line->updated)).date(' Y à H:i',$line->updated);
+					$row['thumbnail'] = $line->thumbnail();
+					$row['icon'] = $line->icon();
+
+					$row['childNumber'] = $line->childNumber;
+			
+					$response['rows'][] = $row;
+				}
+
+				//tri du résultat si demandé
+				if(isset($_['sort'])){
+					$direction = $_['sort']['sort'] == 'asc' ? 1:-1 ;
+					//le in_array permet de s'assurer qu'une colonne triable est spécifiée
+					$attribute = in_array($_['sort']['column'],array('label','size','creator','updated'))? $_['sort']['column']: 'label';
+					usort($response['rows'],function($a,$b) use($attribute,$direction){
+							if($a[$attribute] > $b[$attribute]) return 1*$direction;
+							if($a[$attribute] < $b[$attribute]) return -1*$direction;
+							if($a[$attribute] == $b[$attribute]) return 0;
+					});
+				}
+
+				if($conf->get('document_enable_logs_verbose')) Log::put('Ouverture du dossier '.str_replace(array('/','\\',SLASH.'.'.SLASH.'*'),array(SLASH,SLASH,''),$scanned).' ','document');
+
+				
+
+			}
+
+
+			
+		});
+	break;
+
+	case 'document_element_preview':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('document','read');
+			require_once(__DIR__.SLASH.'Element.class.php');
+			
+			//l'ui ne renvois que les /, on les convertis par le separateur de l'os
+			$_['path'] = str_replace('/', SLASH, $_['path']);
+
+			$path = str_replace(SLASH.'.'.SLASH,SLASH,Element::root().$_['path']);
+			
+			$osPath = File::convert_decoding($path);
+			if(!file_exists($osPath)) throw new Exception('Cet élément a peut-être été modifié ou déplacé par quelqu\'un d\'autre. Rafraîchissez la page et réessayez.');
+			$element = Element::fromPath($path);
+
+			//L'ui ne traite que les / quel que soit l'os
+			$element->path = str_replace('\\', '/', $element->path);
+
+			$row = $element->toArray();
+			$row['updatedRelative'] = relative_time($element->updated);
+			$row['sizeReadable'] = readable_size($element->size);
+			$row['updatedReadable'] = day_name(date('N',$element->updated)).' '. date('d ',$element->updated).month_name(date('m',$element->updated)).date(' Y à H:i',$element->updated);
+			$row['thumbnail'] = $element->thumbnail();
+			$row['icon'] = $element->icon();
+			$row['childNumber'] = $element->childNumber;
+
+
+			$response['row'] = $row;
+		});
+	break;
+
+	case 'document_properties_show':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('document','read');
+			require_once(__DIR__.SLASH.'Element.class.php');
+
+			$element = Element::provide();
+			$element->path = str_replace('\\', '/', $element->path);
+			$row = $element->toArray();
+			$row['createdLabel'] = date('d/m/Y H:i',$element->updated);
+			$row['updatedLabel'] = date('d/m/Y H:i',$element->updated);
+			$bundle = base64_encode(json_encode(array(
+				'root' => $element->path,
+				'folder' => '',
+			)));
+			$row['rootUrl'] = ROOT_URL.'/index.php?module=document&data='.$bundle;
+			$response['row'] = $row;
+		});
+	break;
+
+	case 'document_element_execute':
+		global $myUser,$_,$conf;
+		User::check_access('document','read');
+		require_once(__DIR__.SLASH.'Element.class.php');
+		
+		$isopath =  Element::root().base64_decode(rawurldecode($_['path']));
+		$utf8Path = utf8_encode($isopath);
+		$osPath = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? $isopath : $utf8Path;
+		
+		$stream = Element::download($utf8Path);
+		$name = mt_basename($utf8Path);
+		
+		$mime = 'application/octet-stream';
+		if(is_dir($osPath)){
+			$mime = 'application/zip';
+			$name .= '.zip';
+		}
+		if($conf->get('document_enable_logs_verbose')) Log::put('Téléchargement de '.$utf8Path,'document');
+		File::downloadStream($stream, $name, $mime);
+	break;
+
+
+	case 'document_element_move':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('document','edit');
+			require_once(__DIR__.SLASH.'Element.class.php');
+
+			//l'ui ne renvois que les /, on les convertis par le separateur de l'os
+			$_['from'] = str_replace('/', SLASH, $_['from']);
+			$_['to'] = str_replace('/', SLASH, $_['to']);
+
+			$from =  Element::root().$_['from'];
+			$osFrom =  File::convert_decoding($from);
+			if(!file_exists($osFrom)) throw new Exception('Cet élément a peut-être été modifié ou déplacé par quelqu\'un d\'autre. Rafraîchissez la page et réessayez.');
+			//if($_['to']=='.') $_['to'] = '';
+			
+			$to = Element::root().$_['to'];
+			$osTo = File::convert_decoding($to);
+			if(!is_dir($osTo)) return;
+
+			if(!document_check_element_name(basename(htmlspecialchars_decode(html_entity_decode($to), ENT_QUOTES)))) throw new Exception("Caractères interdits : \\/:*?\"<>|");
+			$to .= SLASH.basename($from);
+
+			$element = Element::move($from,$to);
+			$response['element'] = $element;
+			if($conf->get('document_enable_logs')) Log::put('Déplacement de '.$from.' dans '.$to,'document');
+		});
+	break;
+
+	case 'document_element_rename':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('document','edit');
+			require_once(__DIR__.SLASH.'Element.class.php');
+
+			//les exception vides reset le champ de l'ui sans afficher d'erreur
+			if(!isset($_['label']) || empty($_['label'])) throw new Exception("Le nom ne dois pas être vide");
+
+			if(strlen($_['label']) > 80) throw new Exception("Taille maximale autorisée de 80 caractères.");
+			
+			//l'ui ne renvois que les /, on les convertis par le separateur de l'os
+			$_['path'] = str_replace('/', SLASH, $_['path']);
+			$from =  Element::root().$_['path'];
+			$fromOs =  File::convert_decoding($from);
+			if(!file_exists($fromOs)) throw new Exception('Cet élément a peut-être été modifié ou déplacé par quelqu\'un d\'autre. Rafraîchissez la page et réessayez.');
+
+
+			if(is_dir($fromOs) && substr($_['label'], -1,1)=='.') throw new Exception("Les dossiers ne peuvent pas terminer par un .");
+			
+
+			$to =  dirname($from).SLASH.$_['label'];
+
+			if(file_exists($to)) throw new Exception('Action impossible, un élément existe déjà avec ce nom.');
+				
+
+		
+			if(!document_check_element_name(htmlspecialchars_decode(html_entity_decode($_['label']), ENT_QUOTES))) throw new Exception("Caractères interdits : \\/:*?\"<>|");
+			$element = Element::move($from,$to);
+			
+			if(!$element) throw new Exception("Erreur lors de la récuperation de l'élement renommé", 500);
+			
+			$element->path = str_replace('\\', '/', $element->path);
+			$response['element'] = $element;
+			if($conf->get('document_enable_logs') ) Log::put('Renommage de l\'élément : '.$from.' en '.$to,'document');
+		});
+	break;
+
+	case 'document_element_delete':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('document','delete');
+			require_once(__DIR__.SLASH.'Element.class.php');
+
+			
+			
+			//l'ui ne renvois que les /, on les convertis par le separateur de l'os
+			$path =  Element::root().str_replace('/', SLASH,$_['path']);
+			$osPath =  File::convert_decoding($path);
+			if(!file_exists($osPath)) throw new Exception('Cet élément a peut-être été modifié ou déplacé par quelqu\'un d\'autre. Rafraîchissez la page et réessayez.');
+			Element::remove($path);
+
+			$extension = getExt($path);
+			if( in_array($extension, array('jpg','jpeg','png','gif','bmp'))) {
+
+				$thumbname = str_replace(array('\\'),array('/'),$_['path']);
+				$thumbpath = Element::root().'.thumbnails'.SLASH.base64_encode($thumbname).'.'.$extension;
+				if(file_exists($thumbpath)) unlink($thumbpath);
+			}
+
+			if($conf->get('document_enable_logs')) Log::put("Suppression d'un élément : ".$path,'document');
+		});
+	break;
+
+	//edition d'un fichier (chargement)
+	case 'document_element_edit':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('document','edit');
+			require_once(__DIR__.SLASH.'Element.class.php');
+
+			if(!isset($_['path'])) throw new Exception("Veuillez spécifier le chemin du fichier");
+			$path = str_replace(array('..'),'',$_['path']);
+
+
+			$path = Element::root().$path;
+			$osPath = File::convert_decoding($path);
+
+			if(!file_exists($osPath)) throw new Exception("Impossible de trouver le fichier, peut être a t-il été supprimé entree temps, veuillez recharger la page.");
+			
+			
+			$response['path'] = $path;
+			$response['label'] = mt_basename($path);
+			$response['content'] = Element::download($path);
+
+		});
+	break;
+
+	//edition d'un fichier (sauvegarde)
+	case 'document_element_save':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('document','edit');
+			require_once(__DIR__.SLASH.'Element.class.php');
+
+			if(!isset($_['label'])) throw new Exception("Veuillez spécifier le nom du fichier");
+			$label = str_replace(array('..','/','\\'),'',$_['label']);
+
+
+			$path = Element::root().$_['path'].SLASH;
+			$osPath = File::convert_decoding($path);
+
+			$content = html_entity_decode($_['content']);
+
+			$maxSize = $conf->get('document_allowed_size');
+			$extensions = explode(',',str_replace(' ', '', $conf->get('document_allowed_extensions')));
+			$extension = getExt($_['label']);
+			if(strlen($content) > $maxSize) throw new Exception("Taille du fichier ".$_['label']." trop grande, taille maximum :".readable_size($maxSize).' ('.$maxSize.' octets)');
+			if(!in_array($extension , $extensions)) throw new Exception("Extension '".$extension."' du fichier ".$_['label']." non permise, autorisé :".implode(', ',$extensions));
+			$filePath =  $path.$_['label'];
+			Element::addFile($filePath,$content);
+
+		});
+	break;
+
+	//upload d'un fichier
+	case 'document_element_upload':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('document','edit');
+			require_once(__DIR__.SLASH.'Element.class.php');
+
+			$response['sort'] = $_['sort'];
+
+			if(empty($_FILES)) throw new Exception("Aucun document à importer");
+			
+
+
+			$path = Element::root().$_['path'].SLASH;
+			$osPath = File::convert_decoding($path);
+
+			if(!file_exists($osPath)) throw new Exception("Dossier ".$osPath." introuvable");
+			
+			$maxSize = $conf->get('document_allowed_size');
+			$extensions = explode(',',str_replace(' ', '', $conf->get('document_allowed_extensions')));
+			
+
+
+			
+
+			$extension = getExt($_FILES['file']['name'][0]);
+			if($_FILES['file']['size'][0] > $maxSize) throw new Exception("Taille du fichier ".$_FILES['file']['name'][0]." trop grande, taille maximum :".readable_size($maxSize).' ('.$maxSize.' octets)');
+			if(!in_array($extension , $extensions)) throw new Exception("Extension '".$extension."' du fichier ".$_FILES['file']['name'][0]." non permise, autorisé :".implode(', ',$extensions));
+
+
+			if($_['method'] == 'paste') $_FILES['file']['name'][0] = 'presse papier '.date('d-m-Y H-i-s').'.'.$extension;
+
+			$filePath =  $path.$_FILES['file']['name'][0];
+
+			if(!file_exists($_FILES['file']['tmp_name'][0]))  throw new Exception("Fichier temporaire n°".$_['sort']." inexistant, verifiez la clause upload_max_size de PHP.");
+			
+			
+			
+			Element::addFile($filePath,file_get_contents($_FILES['file']['tmp_name'][0]));
+			
+			if($conf->get('document_enable_logs')) Log::put("Upload d'un élément : ".$filePath,'document');
+		
+
+			
+		});
+	break;
+
+	//Sauvegarde des configurations de document
+	case 'document_setting_save':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('document','configure');
+
+			foreach(Configuration::setting('document') as $key=>$value){
+				if(!is_array($value)) continue;
+				$allowed[] = $key;
+			}
+			foreach ($_['fields'] as $key => $value)
+				if(in_array($key, $allowed)) $conf->put($key,$value);
+		});
+	break;
+
+
+
+	/** ELEMENTRIGHT **/
+	//Récuperation d'une liste de elementright
+	case 'document_right_search':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('document','read');
+			require_once(__DIR__.SLASH.'ElementRight.class.php');
+			
+			$rights = ElementRight::loadAll(array('element'=>$_['id']));
+			foreach($rights as $right){
+				if($right->entity =='rank'){
+					$rank = Rank::getById($right->uid);
+					if(!$rank) continue;
+					$right->uid = $rank->label.' <small class="text-muted">(rang)</small>';
+				}
+				$row = $right->toArray();
+				if($row['read'] == 0) unset($row['read']);
+				if($row['edit'] == 0) unset($row['edit']);
+				if($row['recursive'] == 0) unset($row['recursive']);
+				$response['rows'][] = $row;
+			}
+			
+		});
+	break;
+
+	//Ajout ou modification d'élément elementright
+	case 'document_right_save':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('document','edit');
+			require_once(__DIR__.SLASH.'ElementRight.class.php');
+			require_once(__DIR__.SLASH.'Element.class.php');
+
+			if(!isset($_['uid']) || empty($_['uid'])) throw new Exception("UID de partage non spécifié");
+
+			$element = Element::provide('element');
+			if(!$element) throw new Exception("Cet élément n'existe pas",404);
+			if($element->creator != $myUser->login && !$myUser->can('document','configure') && !$myUser->superadmin) throw new Exception("Vous n'êtes pas propriétaire de cet élement",403);
+
+			$item = ElementRight::provide();
+			$item->element = $element->id;
+			$item->recursive =  isset($_['recursive']) ? $_['recursive'] : 0 ;
+			$item->edit = isset($_['edit']) ? $_['edit'] : 0 ;
+			$item->read = isset($_['read']) ? $_['read'] : 0 ;
+			$item->uid = $_['uid'];
+			$item->entity = is_numeric($_['uid']) ? 'rank' : 'user';
+			//supression des anciens droits sur le même couple element / utilisateur si existants
+			ElementRight::delete(array('element'=>$item->element,'entity'=>$item->entity,'uid'=>$item->uid));
+
+			$item->save();
+		});
+	break;
+	
+	//Suppression d'élement elementright
+	case 'document_right_delete':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('document','delete');
+			require_once(__DIR__.SLASH.'ElementRight.class.php');
+			require_once(__DIR__.SLASH.'Element.class.php');
+			$right = ElementRight::provide('id',1);
+			$element = $right->join('element');
+			if(!$element) throw new Exception("Cet élément n'existe pas",404);
+			if($element->creator != $myUser->login && !$myUser->can('document','configure') && !$myUser->superadmin) throw new Exception("Vous n'etes pas propriétaire de cet élement",403);
+
+			ElementRight::deleteById($right->id);
+		});
+	break;
+
+}
+?>

+ 12 - 0
plugin/document/app.json

@@ -0,0 +1,12 @@
+{
+	"id": "fr.sys1.document",
+	"name": "Documents",
+	"author" : {
+		"name" : "Valentin Carruesco"
+	},
+	"version": "2.0",
+	"url": "http://sys1.fr",
+	"licence": {"name": "Copyright","url" : ""},
+	"description": "Gestion Électronique de Documents (GED) partagée sur l'ERP",
+	"require" : {}
+}

+ 885 - 0
plugin/document/css/document.api.css

@@ -0,0 +1,885 @@
+/** DOCUMENT **/
+.document-container{
+	display: flex;
+	flex:1;
+	/*100vh - 50px (header) - 50px (footer)*/
+	height: calc(100vh - 100px);
+	user-select: none;
+}
+
+.document-container.embedded {
+	margin: 0px;
+}
+
+.document-container ul{
+	list-style-type: none;
+	margin:0;
+	padding:0;
+}
+
+
+.document-container .file-preloader{
+	text-align: center;
+    color: #cecece;
+    font-size: 40px;
+    position: absolute;
+    width: 100%;
+    left:0;
+    top:100px;
+    padding: 50px;
+ 
+}
+
+.document-container .file-preloader i{
+	/*background-color: #ffffff;*/
+	padding:5px;
+	border-radius: 100px;
+}
+
+.document-container .file-preloader .fa-spin {
+    animation: fa-spin 0.6s infinite linear;
+}
+
+.document-container .tree-panel {
+	width: 15%;
+	order:1;
+	padding: 15px;
+	color:#444444;
+	background-color: #f5f4f4;
+	/*background-color: #ddedff;*/
+	font-size: 15px;
+	border-right:1px solid #cecece;
+	/*100vh - 50px (header) - 50px (footer)*/
+	height: calc(100vh - 100px);
+	overflow-y: auto;
+	resize: horizontal;
+}
+
+.document-container .tree-panel > ul li.folder.folder-open>i.far.fa-folder,
+.document-container .tree-panel > ul li.folder.folder-open>i.far.fa-folder-open {
+	font-weight: normal;
+}
+
+
+.document-container .tree-panel > ul li.folder.folder-open>i::before {
+	content: "\f07c"!important;
+}
+
+
+
+
+.document-container i.far.fa-folder,
+.document-container i.far.fa-folder-open {
+	color: #f1c40f;
+	font-weight: bold;
+}
+
+.document-container .tree-panel > ul li i.fa-folder-open,.document-container .tree-panel > ul li i.fa-folder{
+	display: inline-block;
+	width: 15px;
+}
+
+.document-container .tree-panel > ul li.folder:before {
+	content :'\f105';
+	padding-right:10px;
+	color:#aeaeae;
+	font-family: 'Font Awesome 5 Free';
+	font-weight: 900;
+	display: inline-block;
+    width: 15px;
+	transition: all 0.2s ease-in-out;
+	opacity: 0;
+}
+
+
+.document-container .tree-panel > ul li.folder.folder-open:before {
+	content:'\f107';
+	padding-right:6px;
+	color:#444444;
+	font-family: 'Font Awesome 5 Free';
+}
+.document-container .tree-panel > ul li.folder:hover:before {
+	
+	opacity:1;
+}
+
+
+.document-container .tree-panel > ul li.folder {
+	transition: transform 0.1s linear;
+	word-break: break-all;
+	text-overflow: ellipsis;
+	overflow: hidden;
+	white-space: nowrap;
+}
+
+.document-container .tree-panel  ul li.folder-focused {
+	font-weight: bold;
+}
+
+.document-container .tree-panel  ul li.folder-hover {
+	font-weight: bold;
+}
+.document-container .tree-panel  ul li.folder-hover:before {
+	content:'\f107';
+	padding-right:6px;
+	color:#aeaeae;
+	font-family: 'Font Awesome 5 Free';
+	font-weight: 900;
+}
+
+.document-container .tree-panel ul li.folder-focused ul {
+	font-weight: normal;
+}
+
+
+/* VUE LISTE */
+
+.document-container  .file-elements-list tbody tr {
+	cursor: pointer;
+	transition: background 0.1s ease-in-out;
+}
+.document-container  .file-elements-list tbody tr:hover {
+	background-color: #f8f8f8;
+}
+
+.document-container  .file-elements-list tbody tr .element-rename {
+	opacity: 0;
+	transform: translateX(-10px);
+	margin-left: 10px;
+	cursor: pointer;
+	color:#cecece;
+	transition: all 0.2s ease-in-out;
+}
+.document-container  .file-elements-list tbody tr:hover .element-rename {
+	transform: translateX(0);
+	opacity:1;
+}
+
+
+.document-container  .file-elements-list tbody tr.folder-receive-element {
+	background-color:#bad4ff;
+}
+
+.file-elements-list .rename-input,.file-elements-list .rename-input:focus,.file-elements-list .rename-input:active{
+	border:0px;
+	padding:0;
+	outline: none;
+}
+
+.document-container  .file-elements-list .element-focused {
+	font-weight: bold;
+	background-color: #f8f8f8;
+}
+.document-container .file-elements-list .element-thumbnail {
+	height: 15px;
+	width: 15px;
+	margin-right: 5px;
+}
+/* VUE GRILLE*/
+
+.document-container  .file-elements-grid > li {
+	cursor: pointer;
+	display: inline-block;
+	vertical-align: top;
+	transition: background 0.1s ease-in-out;
+	padding:15px;
+	text-align: center;
+	
+	overflow: hidden;
+}
+
+.document-container ul.file-elements-grid{
+	text-align: center;
+}
+
+
+.file-module[data-view="grid"] {
+    background-color: #f8f8f8;
+}
+
+.document-container  .file-elements-grid .name-cell > span > span{
+	width: calc(100% - 45px)!important;
+    display: inline-block;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+}
+
+
+.document-container  .file-elements-grid > li > .grid-container{
+	border-radius:3px;
+	width:250px;
+	height:200px;
+	background-color:#ffffff;
+	padding: 15px 5px 5px 5px;
+	border:1px solid #fefefe;
+	box-shadow: 0 1px 4px rgba(165, 165, 165, 0.4);
+	transition: all 0.2s ease-in-out;
+	position: relative;
+}
+
+.document-container  .file-elements-grid  .element-thumbnail  {
+	border-radius: 5px 5px 0 0;
+    background-repeat: no-repeat;
+    background-position:center center;
+    margin: -15px -6px 0 -6px;
+    height: 100px;
+}
+.document-container  .file-elements-grid .element-type-jpg  .element-thumbnail,
+.document-container  .file-elements-grid .element-type-jpeg  .element-thumbnail,
+.document-container  .file-elements-grid .element-type-png  .element-thumbnail,
+.document-container  .file-elements-grid .element-type-gif  .element-thumbnail,
+.document-container  .file-elements-grid .element-type-bmp  .element-thumbnail
+  {
+    background-size: cover;
+    position: absolute;
+    width:250px;
+	height:200px;
+	z-index: 0;
+	border-radius: 5px;
+}
+.document-container  .file-elements-grid .element-type-jpg  .element-infos,
+.document-container  .file-elements-grid .element-type-jpeg  .element-infos,
+.document-container  .file-elements-grid .element-type-png  .element-infos,
+.document-container  .file-elements-grid .element-type-gif  .element-infos,
+.document-container  .file-elements-grid .element-type-bmp  .element-infos
+  {
+   color:#ffffff;
+    position: absolute;
+    background: rgba(0,0,0,0.5);
+    width:100%;
+    bottom:0;
+    border-radius:0 0 5px 5px;
+    margin-left: -5px;
+    margin-bottom: -2px;
+    margin-right: -5px;
+	z-index: 1;
+}
+.document-container  .file-elements-grid .element-type-jpg  .name-cell,
+.document-container  .file-elements-grid .element-type-jpeg  .name-cell,
+.document-container  .file-elements-grid .element-type-png  .name-cell,
+.document-container  .file-elements-grid .element-type-gif  .name-cell,
+.document-container  .file-elements-grid .element-type-bmp  .name-cell
+ {
+	color:#ffffff;
+}
+
+
+.document-container  .view-module{
+	position: absolute;
+	right: 10px;
+}
+
+.document-container  .view-module > li{
+	cursor: pointer;
+	display: inline-block;
+	vertical-align: top;
+	padding:5px;
+	opacity: 0.8;
+	transition: all 0.2s ease-in-out;
+}
+
+.document-container  .view-module > li:hover{
+	opacity: 1;
+	transform: scale(1.1);
+}
+.document-container  .view-module > li.selected{
+	color:#007bff;
+}
+
+.document-container  .file-elements-grid .name-cell{
+	margin-top:5px;
+	font-weight: bold;
+	color: rgb(0, 123, 255);
+	font-size: 12px;
+}
+
+.document-container  .file-elements-grid  .creator-cell,
+.document-container  .file-elements-grid  .size-cell,
+.document-container  .file-elements-grid  .updated-cell{
+	font-size:11px;
+	color:#cecece;
+}
+
+.document-container  .file-elements-grid .name-cell{
+	margin-top:5px;
+	font-weight: bold;
+	color: rgb(0, 123, 255);
+	font-size: 12px;
+}
+
+
+.document-container  .file-elements-grid  > li .grid-container:hover {
+	/*background-color: #f8f8f8;*/
+	transform: scale(1.05);
+}
+
+.document-container  .file-elements-grid .grid-container .element-rename {
+	opacity: 0;
+	transform: translateX(-10px);
+	margin-left: 10px;
+	position: absolute;
+	cursor: pointer;
+	color:#cecece;
+	transition: all 0.2s ease-in-out;
+}
+.document-container  .file-elements-grid > li .name-cell:hover .element-rename {
+	transform: translateX(0);
+	opacity:1;
+}
+
+
+.document-container  .file-elements-grid > li.folder-receive-element  .grid-container{
+	background-color:#bad4ff;
+}
+
+.file-elements-grid .rename-input,.file-elements-grid .rename-input:focus,.file-elements-grid .rename-input:active{
+	border:0px;
+	padding:0;
+	outline: none;
+}
+
+.document-container  .file-elements-grid .element-focused .grid-container{
+	box-shadow: 0 1px 4px #00adff;
+}
+
+
+/**/
+
+.dragging-element{
+	opacity: 0.5;
+}
+.original-placeholder {
+	display: table-row;
+	opacity: 0.5;
+	background-color: #deebff !important;
+}
+.hidden-rename {
+	display:none;
+	white-space:pre;
+}
+
+
+
+
+
+.document-container .tree-panel > ul li.folder {
+	cursor:pointer;
+}
+
+.document-container .tree-panel > ul li ul {
+	padding-left: 10px;
+}
+
+.document-container .file-panel {
+	flex: 1;
+	order: 2;
+	border-right:1px solid #cecece;
+	position: relative;
+	z-index: 100;
+}
+
+
+.document-create-dropdown .dropdown-item.active,.document-create-dropdown .dropdown-item:active,
+.document-create-dropdown .dropdown-item:hover{
+	background: inherit;
+	color: inherit;
+}
+
+/* upload panel */
+
+
+.upload-state{
+	color: #fff;
+	margin:5px 0 0 80px;
+}
+
+
+.document-container .upload-files{
+    color: #fff;
+    width: 500px;
+    margin-top:20px;
+    max-height: 400px;
+    overflow-y: auto;
+}
+
+.document-container .upload-files > li > div{
+	width:100%;
+}
+
+.document-container .upload-files h5{
+	font-weight: bold;
+	font-size: 14px;
+	line-height: 18px;
+	margin:0 5px 0 0;
+	padding:0;
+	float:left;
+}
+.document-container .upload-files small{
+	opacity:0.5;
+	margin-right:5px;
+	float:left;
+}
+
+.document-container .upload-files .progress {
+    margin: 7px 0;
+    border-radius: 5rem;
+    border: 1px solid #fff;
+    background-color: transparent;
+    height: 12px;
+    width: 100%;
+    float:left;
+}
+
+
+.document-container .upload-files .progress .progress-bar {
+    color: #444;
+    background-color: #fff;
+}
+
+.document-container .upload-file-state{
+		float:right;
+		font-size: 13px;
+}
+.document-container .upload-file-state:after{
+	clear: both;
+	display: block;
+	content: ' ';
+}
+.document-container .upload-file-state.success
+{
+	color: #0bc60b;
+}
+
+.document-container .upload-file-state.error {
+	color: #ff6b79;
+}
+
+.preloader-upload-container .preloader-upload-close{
+	font-size: 30px;
+    position: absolute;
+    right: 20px;
+    top: 65px;
+    cursor: pointer;
+    color: #d0d0d0;
+}
+
+.preloader-upload-container .upload-state h5{
+	 font-weight: bold;
+    font-size: 14px !important;
+    text-transform: uppercase;
+    font-size: 1.25rem;
+    color: #a9a9a9;
+}
+
+.drag-overlay,
+.preloader-upload-container {
+	position: fixed; /* Sit on top of the page content */
+	display: none; /* Hidden by default */
+	width: 100%; /* Full width (cover the whole page) */
+	height: 100%; /* Full height (cover the whole page) */
+	top: 0; 
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0,0,0,0.7); /* Black background with opacity */
+	z-index: 10000; /* Specify a stack order in case you're using a different order for other elements */
+	cursor: pointer; /* Add a pointer on hover */
+}
+
+.overlay-text {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	font-size: 30px;
+	color: white;
+	transform: translate(-50%,-50%);
+	-ms-transform: translate(-50%,-50%);
+	z-index: 10000;
+}
+
+.overlay-icon {
+	position: absolute;
+	top: 52%;
+	left: 49%;
+	font-size: 60px;
+	font-weight: lighter;
+	color: #ffc700;
+	transform: translate(-49%,-52%);
+	-ms-transform: translate(-49%,-52%);
+	z-index: 10000;
+	animation: floating 1.5s ease-in-out infinite;
+}
+
+.upload-button > div > i.far.fa-file-alt {
+	color: #007bff;
+}
+
+.file-form input.label-required {
+	background: rgba(229, 35, 37, 0.25);
+}
+.file-form input.label-required::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
+	color: red;
+	opacity: 1; /* Firefox */
+}
+
+.file-form input.label-required:-ms-input-placeholder { /* Internet Explorer 10-11 */
+	color: red;
+}
+
+.file-form input.label-required::-ms-input-placeholder { /* Microsoft Edge */
+	color: red;
+}
+
+@keyframes floating {
+	from { transform: translate(0,  0px); }
+	50%  { transform: translate(0, 15px); }
+	to   { transform: translate(0, -0px); }    
+}
+
+.document-container .file-panel.drag-over {
+	box-shadow: 0 0 5px rgba(0,0,0,0.5);
+}
+.doc-search-container {
+	position: relative;
+	width: 50%;
+}
+.doc-search-container .btn-search {
+    position: absolute;
+    right: 10px;
+    top: 5px;
+    bottom: 0;
+    height: 25px;
+    margin: auto;
+    font-size: 14px;
+    cursor: pointer;
+    color: #a0a0a0;
+    z-index: 4;
+    transition: all 0.15s ease-in;
+}
+
+.document-container .search-clear {
+	opacity: 0;
+	transform: scale(0);
+	position: absolute;
+	right: 33px;
+	top: 12px;
+	bottom: 0;
+	height: 25px;
+	margin: auto;
+	font-size: 14px;
+	cursor: pointer;
+	color: #a0a0a0;
+	z-index: 4;
+	transition: all 0.15s ease-in;
+}
+
+.doc-search-container.typing .search-clear {
+	opacity: 1;
+	transform: scale(1);
+}
+
+.input-group-append {
+	z-index: 5;
+}
+.file-panel .breadcrumb-module {
+	padding: 0 15px 0 15px;
+	margin: 5px 0;
+	color:#666666;
+}
+.file-panel .breadcrumb-module > ul li:not(:last-of-type) {
+	background-image: url(../img/breadcrumb.svg);
+}
+.file-panel .breadcrumb-module > ul li{
+    display: inline-block;
+    padding: 8px 15px 0 0;
+    margin: 0 0 0 5px;
+    background-repeat: no-repeat;
+    background-position: right center;
+    height: 40px;
+    background-size: auto 20px;
+    cursor: pointer;
+}
+.file-panel .search-module {
+	padding: 0 15px 0 15px;
+	margin:0px 0 10px 0 ;
+	color:#aeaeae;
+	position: relative;
+}
+.file-panel .search-module .doc-search-container input {
+	width:100%;
+	border-radius: 2em;
+}
+.document-container .detail-panel {
+	width: 20%;
+	overflow-y: auto;
+	order:3;
+	border-right:1px solid #cecece;
+}
+
+.document-container .detail-panel::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+.document-container .detail-panel::-webkit-scrollbar-button {
+  width: 0px;
+  height: 0px;
+}
+.document-container .detail-panel::-webkit-scrollbar-thumb {
+  background: #cecece;
+  border: 0px none #ffffff;
+  border-radius: 50px;
+}
+.document-container .detail-panel::-webkit-scrollbar-thumb:hover {
+  background: #707070;
+}
+.document-container .detail-panel::-webkit-scrollbar-thumb:active {
+  background: #949494;
+}
+.document-container .detail-panel::-webkit-scrollbar-track {
+  background: #ffffff;
+  border: 0px none #ffffff;
+  border-radius: 50px;
+}
+.document-container .detail-panel::-webkit-scrollbar-track:hover {
+  background: #d4d4d4;
+}
+.document-container .detail-panel::-webkit-scrollbar-track:active {
+  background: #d3d3d3;
+}
+.document-container .detail-panel::-webkit-scrollbar-corner {
+  background: transparent;
+}
+
+
+
+.detail-panel h1{
+	font-size: 18px;
+	margin: 5px 0;
+}
+.document-container .document-add-element{
+	display:inline-block;
+	margin-left:10px;
+}
+.document-container .document-add-element > button{
+	border-radius: 2em;
+	padding-top: 4px;
+	padding-bottom: 4px;
+}
+.documents-settings {
+	position: absolute;
+	right: 10px;
+}
+.file-module {
+	border-top: 1px solid #cecece;
+	/*100vh - 50px (header) - 50px (footer) - 92px (breadcrumb + search)*/
+	height: calc(100vh - 192px);
+	overflow-y: auto;
+}
+.file-module > table {
+	width:100%;
+}
+.file-module > table th,
+.file-module > table td{
+	padding: 5px 10px;
+}
+.file-module > table thead th {
+	padding: 10px;
+    cursor: pointer;
+    transition: all 0.1s ease-in-out;
+}
+.file-module > table thead th:hover {
+	color: #8c8c8c;
+    background-color: #ddedff;
+}
+.file-module > table thead {
+	color:#b2b2b2;
+    font-size: 0.75em;
+}
+.file-module > table thead tr {
+	border-bottom:1px solid #eaeaea;
+}
+.file-module > table thead tr th.name-head,
+.file-module > table tbody tr td.name-cell {
+	padding-left: 20px;
+}
+.file-module > table tbody tr td.name-cell {
+	font-size: 0.8em;
+	width: 350px;
+	max-width: 350px;
+	text-overflow: ellipsis;
+	overflow: hidden; 
+	white-space: nowrap;
+}
+
+.file-module > table tbody tr td.size-cell {
+	width: 100px;
+	font-size: 11px;
+}
+.file-module > table tbody tr td.creator-cell,
+.file-module > table tbody tr td.firm-cell,
+.file-module > table tbody tr td.updated-cell {
+	width: 150px;
+	font-size: 11px;
+}
+.file-module > table tbody tr {
+	border-bottom:1px solid #eaeaea;
+	color:#595959;
+}
+.file-module > table tbody tr.ui-sortable-placeholder {
+	display: none;
+}
+
+.detail-panel .detail-thumbnail{
+	background: url(../img/default-image.svg) no-repeat center center #f5f4f4;
+	height:250px;
+	
+	text-align: center;
+	background-size: auto 140px; 
+}
+.detail-panel .detail-thumbnail .thumbnail-preloader{
+	font-size: 40px;
+	height:250px;
+	background-color: #f5f4f4;
+	padding-top: 100px;
+	opacity:0;
+	box-sizing: border-box;
+	color:#cecece;
+	transition: opacity 0.2s ease-in-out;
+	
+}
+.detail-panel .detail-thumbnail .thumbnail-preloader.show{
+ 	opacity:1;
+ 	transition:none;
+}
+
+
+.document-container .detail-buttons{
+	padding: 5px 10px;
+}
+.document-container .detail-buttons li .btn{
+	width:100%;
+}
+.detail-panel h1{
+	padding: 5px 10px;
+	color: #333333;
+	word-break: break-all;
+}
+.detail-panel small.informations {
+	display: inline-block;
+	width: 100%;
+	color: #bebebe;
+	padding: 5px 10px;
+}
+
+.document-container .file-editor{
+	position: absolute;
+	background: #fefefe;
+	width: 100%;
+	height: calc(100vh - 192px);
+	z-index: 10;
+}
+.document-container .file-editor .file-editor-input{
+	width: 100%;
+	height:500px;
+	padding:15px;
+	color:#222222;
+	outline: none;
+}
+
+.document-container .btn-editor-save,.btn-editor-cancel{
+	border-radius: 0px;
+	float: right;
+}
+
+.document-container .file-editor .file-editor-header{
+	background: #f5f4f4;
+}
+
+.document-container .file-editor .file-editor-name{
+	margin-left: 5px;
+	width:200px;
+	font-weight: bold;
+	height: 38px;
+	opacity:0.5;
+	transition: opacity 0.2s ease-in-out;
+	outline: none;
+	background: transparent;
+	border:0;
+}
+.document-container .file-editor .file-editor-name:hover{
+	opacity:1;
+}
+
+@keyframes pop{
+	50%  {transform: scale(1.2);}
+}
+
+
+.preloader-upload {
+	position: absolute;
+	top: calc(30% - 20px);
+	left: calc(50% - 250px);
+}
+@keyframes loader {
+	0% { left: -100px }
+	100% { left: 110%; }
+}
+.preloader-upload-box {
+	width: 50px;
+	height: 50px;
+	background: #ffc700;
+	animation: animate .5s linear infinite;
+	position: absolute;
+	top: 0;
+	left: 0;
+	border-radius: 3px;
+}
+@keyframes animate {
+	17% { border-bottom-right-radius: 3px; }
+	25% { transform: translateY(9px) rotate(22.5deg); }
+	50% {
+		transform: translateY(18px) scale(1,.9) rotate(45deg) ;
+		border-bottom-right-radius: 40px;
+	}
+	75% { transform: translateY(9px) rotate(67.5deg); }
+	100% { transform: translateY(0) rotate(90deg); }
+} 
+.preloader-upload-shadow { 
+	width: 50px;
+	height: 5px;
+	background: #000;
+	opacity: 0.1;
+	position: absolute;
+	top: 59px;
+	left: 0;
+	border-radius: 50%;
+	animation: shadow .5s linear infinite;
+}
+@keyframes shadow {
+	50% {
+		transform: scale(1.2,1);
+	}
+}
+
+
+@media (max-width: 768px){
+	.module-document {
+    overflow: auto;
+	}
+	.document-container{
+		display: block;
+	}
+	.document-container .detail-panel {
+	    display: block!important;
+	    width: 100%;
+	}
+	.document-container .detail-thumbnail {
+	
+	   height:200px;
+	}
+
+	.footer{
+		display: none;
+	}
+}

+ 7 - 0
plugin/document/css/main.css

@@ -0,0 +1,7 @@
+.module-document{
+	overflow: hidden;
+}
+
+.module-document .document-container{
+	margin: -16px -15px 0 -15px;
+}

+ 3 - 0
plugin/document/css/widget.css

@@ -0,0 +1,3 @@
+.widgetDocumentContainer .document-container,.widgetDocumentContainer .document-container .file-module{
+	height:auto;
+}

+ 349 - 0
plugin/document/document.plugin.php

@@ -0,0 +1,349 @@
+<?php
+
+
+//Déclaration d'un item de menu dans le menu principal
+function document_menu(&$menuItems){
+	global $_,$myUser;
+
+	if(!$myUser->can('document','read')) return;
+	$menuItems[] = array(
+		'sort'=>4,
+		'url'=>'index.php?module=document',
+		'label'=>'Documents',
+		'icon'=> 'far fa-copy',
+		'color'=> '#FFBA00'
+	);
+}
+
+//Cette fonction va generer une page quand on clique sur document dans menu
+function document_page(){
+	global $_,$myUser;
+	if(!isset($_['module']) || $_['module'] !='document') return;
+	$page = !isset($_['page']) ? 'list' : $_['page'];
+	$file = __DIR__.SLASH.'page.'.$page.'.php';
+	if(!file_exists($file)) throw new Exception("Page ".$page." inexistante");
+	require_once($file);
+}
+
+//Fonction executée lors de l'activation du plugin
+function document_install($id){
+	if($id != 'fr.sys1.document') return;
+	Entity::install(__DIR__);
+	if(!file_exists(Element::root()))
+		mkdir(Element::root(),0755,true);
+
+	if(!file_exists(__DIR__.SLASH.'logs'))
+		mkdir(__DIR__.SLASH.'logs',0755,true);
+
+	global $conf;
+	$conf->put('document_allowed_extensions','csv,xls,xlsx,doc,docx,dotm,dotx,pdf,png,jpg,jpeg,gif,svg,bmp,txt,zip,msg');
+	$conf->put('document_allowed_size',10485760);
+}
+
+//Fonction executée lors de la désactivation du plugin
+function document_uninstall($id){
+	if($id != 'fr.sys1.document') return;
+	Entity::uninstall(__DIR__);
+}
+
+//Déclaration des sections de droits du plugin
+function document_section(&$sections){
+	$sections['document'] = "Gestion des droits sur le plugin document";
+}
+
+//Cette fonction comprend toutes les actions du plugin qui ne nécessitent pas de vue html
+function document_action(){
+	require_once(__DIR__.SLASH.'action.php');
+}
+
+//Déclaration du menu de réglages
+function document_menu_setting(&$settingMenu){
+	global $_, $myUser;
+	
+	if($myUser->can('document','configure')) {
+		$settingMenu[]= array(
+			'sort' =>4,
+			'url' => 'setting.php?section=document',
+			'icon' => 'fas fa-angle-right',
+			'label' => 'Documents'
+		);
+	}
+}
+
+//Déclaration des pages de réglages
+function document_content_setting(){
+	global $_;
+	if(file_exists(__DIR__.SLASH.'setting.'.$_['section'].'.php'))
+		require_once(__DIR__.SLASH.'setting.'.$_['section'].'.php');
+}
+
+//Vérifie qu'un nom de fichier ou de dossier ne contient pas des caractères interdits par l'os (?:*|<>/\)
+function document_check_element_name($element){
+	return preg_match('|[\/\\\:\*\?\"\<\>\|]|i', $element) == 0;
+}
+
+function document_dav_document($requested){
+	global $conf,$myUser;
+	$requested = trim($requested, '/');
+
+	if(substr($requested, 0,13)!='dav/documents') return;
+	if(empty($requested)) throw new Exception("Unspecified DAV path");
+    
+	if($conf->get('document_enable_dav') != "1"){
+		header('HTTP/1.1 501 Method not implemented');
+	    header('Content-Type: text/html; charset=utf-8');
+		throw new Exception('Mode Webdav désactivé, veuillez débloquer le Webdav dans les paramétrages pour accéder à cette fonctionnalité');
+	}
+
+	require_once(__ROOT__.'common.php');
+	require_once(__DIR__.SLASH.'Element.class.php');
+	require_once(__DIR__.SLASH.'WebDav.class.php');
+
+	$projectPath = preg_replace('|https?\:\/\/'.$_SERVER['HTTP_HOST'].'|i', '', ROOT_URL);
+
+	$server = new WebDav();
+	$server->logs = WebDav::logPath().SLASH.'dav-logs.txt';
+	$server->lockfile = WebDav::logPath().SLASH.'dav-lock.json';
+	$server->root = str_replace('//','/',$projectPath.'/dav/documents/');
+	$server->folder = Element::root();
+
+	//Windows cherche desktop.ini a chaque ouverture de dossier pour le custom
+	$server->ignoreFiles[] = 'desktop.ini';
+	//Tortoise et autre client git d'explorer cherchent les .git a chaques ouverture
+	$server->ignoreFiles[] = '.git';
+
+	if($conf->get('document_enable_dav_logs') == "1"){
+		$server->on['log'] = function($message){
+			Log::put(nl2br($message),'DAV');
+		};
+	}
+
+	$server->do['login'] = function($login,$password){
+		global $myUser;
+
+		if($myUser->login !=''){
+			//file_put_contents(WebDav::logPath().SLASH.'dav-logs.txt', 'already connected with : '.$login.PHP_EOL,FILE_APPEND);
+			return $myUser;
+		}
+		
+		// file_put_contents(WebDav::logPath().SLASH.'dav-logs.txt', 'Connecting with : '.$login.PHP_EOL,FILE_APPEND);
+		$myUser = User::check($login,$password);
+
+		if(!$myUser)
+			throw new Exception("Problème lors de la connexion, veuillez contacter l'administrateur",401);
+		
+		if(file_exists('enabled.maintenance') && $myUser->superadmin != 1)
+			throw new Exception('Seul un compte Super Admin peut se connecter en mode maintenance',401);
+		
+		if(!$myUser->connected())
+			throw new Exception('Identifiant ou mot de passe incorrect',401);
+
+		if(is_numeric($myUser->preference('default_firm')) && $myUser->haveFirm($myUser->preference('default_firm'))){
+			$_SESSION['firm'] = serialize(Firm::getById($myUser->preference('default_firm')));
+			if(!$_SESSION['firm'])
+				throw new Exception("Problème lors de la connexion, veuillez contacter l'administrateur",401);
+			
+		}else if(count($myUser->firms)!=0){
+			$_SESSION['firm'] = serialize(reset($myUser->firms));
+			if(!$_SESSION['firm'])
+				throw new Exception(" Problème lors de la connexion, veuillez contacter l'administrateur",401);
+		}else{
+			throw new Exception('Ce compte n\'est actif sur aucune firm',401);
+		}			
+		
+		$myFirm = isset($_SESSION['firm']) ? unserialize($_SESSION['firm']) : new Firm();
+		if(!$myFirm)
+			throw new Exception("Problème lors de la connexion, veuillez contacter l'administrateur",401);
+
+		$_SESSION['currentUser'] = serialize($myUser);
+		if(!$_SESSION['currentUser'])
+			throw new Exception("Problème lors de la connexion, veuillez contacter l'administrateur",401);
+	};
+
+	$server->do['delete'] = function($isoPath){
+		global $myUser,$conf;
+		$utf8Path = utf8_encode($isoPath);
+		Element::remove($utf8Path);
+		if($conf->get('document_enable_logs')) Log::put('Suppression de '.$utf8Path,'document');
+	};
+
+	$server->do['download'] = function($isoPath){
+		global $myUser,$_,$conf;
+		$utf8Path = utf8_encode($isoPath);
+
+		//pour verfiier si le fichier existe, on récupere son chemin système avec le bon encodage
+		$osPath = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? $isoPath: $utf8Path; 
+
+		if(!file_exists($osPath))  throw new Exception("Fichier inexistant",404);
+	    if(!is_file($osPath)) throw new Exception("Méthode non autorisée sur autre chose qu'un fichier",501);
+
+		$stream = Element::download($utf8Path);
+
+		if($conf->get('document_enable_logs_verbose')) Log::put('Téléchargement de '.$utf8Path,'document');
+
+		return $stream;
+	};
+
+	$server->do['edit'] = function($isoPath,$body,$type){
+		global $myUser,$conf;
+		User::check_access('document','edit');
+		$utf8Path = utf8_encode($isoPath);
+
+		if($type=='file'){
+			$maxSize = $conf->get('document_allowed_size');
+			$extension = getExt($utf8Path);
+			$extensions = explode(',',str_replace(' ', '', $conf->get('document_allowed_extensions')));
+			if(strlen($body) > $maxSize) throw new Exception("Taille du fichier  trop grande, taille maximum :".readable_size($maxSize).' ('.$maxSize.' octets)',406);
+			if(!in_array($extension , $extensions)) throw new Exception("Extension '".$extension."' du fichier ".$path." non permise, autorisé :".implode(', ',$extensions),415);
+			$element = Element::addFile($utf8Path, $body);
+			if($conf->get('document_enable_logs') || $conf->get('document_enable_logs_verbose')) Log::put('Enregistrement fichier '.$utf8Path,'document');
+		}else{
+			Element::addFolder($utf8Path);
+			if($conf->get('document_enable_logs') || $conf->get('document_enable_logs_verbose')) Log::put('Enregistrement dossier '.$utf8Path,'document');
+		}
+
+		$element = Element::fromPath($utf8Path);
+		$baseElement = Element::load(array('path'=>$element->path));
+		if(is_object($baseElement) && $baseElement->id!=0) $element->id = $baseElement->id;
+		$element->save();
+	};
+
+	$server->do['move'] = function($isoPath,$isoTo){
+		global $myUser,$conf;
+		$utf8Path = utf8_encode($isoPath);
+		$utf8To = utf8_encode($isoTo);
+		User::check_access('document','edit');
+		if(!document_check_element_name(mt_basename($utf8To), ENT_QUOTES)) throw new Exception("Caractères interdits : \\/:*?\"<>|");
+		Element::move($utf8Path,$utf8To);
+		if($conf->get('document_enable_logs') || $conf->get('document_enable_logs_verbose')) Log::put('Déplacement de '.$utf8Path.' dans '.$utf8To,'document');
+	};
+
+	$server->do['copy'] = function($isoPath,$isoTo){
+		global $myUser,$conf;
+		$utf8Path = utf8_encode($isoPath);
+		$utf8To = utf8_encode($isoTo);
+		User::check_access('document','edit');
+		if(!document_check_element_name(mt_basename($utf8To), ENT_QUOTES)) throw new Exception("Caractères interdits : \\/:*?\"<>|");
+		Element::copy($utf8Path,$utf8To);
+		if($conf->get('document_enable_logs') || $conf->get('document_enable_logs_verbose')) Log::put('Copie de '.$utf8Path.' dans '.$utf8To,'document');
+	};
+
+	$server->do['list'] = function($isoPath,$depth =0){
+		global $myUser,$conf;
+		$utf8Path = utf8_encode($isoPath);
+		User::check_access('document','read');
+
+		//pour verfier si le fichier existe, on récupere son chemin système avec le bon encodage
+		$osPath = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? $isoPath: $utf8Path; 
+		if(!file_exists($osPath)) throw new Exception("Not found", 404);
+
+		$files = array();
+
+		//Si la cible est la racine dav, on la retourne sous forme d'element fictif, sinon on utilise le systeme d'element standard
+		if($utf8Path == Element::root()){
+			$file = new Element();
+			$file->label = '';
+			$file->type = 'directory';
+			$file->path = '';
+			$toScan = array($file);
+		}else{
+			$toScan = array(Element::fromPath($utf8Path));
+		}
+
+		if($conf->get('document_enable_logs_verbose')) Logs::put('visualisation de '.$utf8Path,'document');
+
+		//Si le dav demande un scan en profondeur du dossier, on scan les enfants du dossier ciblé
+		if($depth>0)
+			$toScan = array_merge($toScan,Element::browse($utf8Path.SLASH.'*'));
+		
+		foreach($toScan as $element){
+			//on convertis l'utf8 de l'element pour passer en iso webdav windows si le serveur est sous windows
+			$path = Element::root().(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? utf8_decode( $element->path) : $element->path );
+
+		    $file = array(
+				'type' => $element->type,
+				'label' =>  $element->label
+		    );
+
+		    if($element->type == 'directory'){
+		    	$stat = stat($path);
+				$file['create_time']  = $stat['ctime'];
+				$file['update_time']  = $stat['mtime'];
+				$file['length'] =  $stat['size'];
+			}else{
+				$file['create_time']  =  filectime($path);
+				$file['update_time']  =  filemtime($path);
+				$file['length'] =  filesize($path);
+			}
+
+			$files[] = $file;
+		}
+
+		return $files;
+	};
+
+	$server->start();
+}
+
+//Déclaration des settings de base
+//Types possibles : text,select ( + "values"=> array('1'=>'Val 1'),password,checkbox. Un simple string définit une catégorie.
+Configuration::setting('document',array(
+    "Général",
+    'document_allowed_extensions' => array("label"=>"Extensions autorisées","type"=>"text","legend"=>"(séparés par virgules)","placeholder"=>"csv,pdf,jpg,..."),
+    'document_allowed_size' => array("label"=>"Taille maximum autorisée","type"=>"text","legend"=>"(en octets)","placeholder"=>"28060000","type"=>"number"),
+
+    'document_enable_logs' => array("label"=>"Activer les logs standards","legend"=>"(ajout, supression, modifification d'un fichier ou dossier)","type"=>"checkbox"),
+    'document_enable_logs_verbose' => array("label"=>"Activer les logs avancés","legend"=>"(lecture ou téléchargement d'un fichier ou dossier)","type"=>"checkbox"),
+    "Options DAV <br><small style='font-size:65%;' class='text-muted'>Adresse WebDAV : <code>".
+    ( substr(ROOT_URL,-1) == '/' ? substr(ROOT_URL,0,(strlen(ROOT_URL)-1)) : ROOT_URL )
+    ."/dav/documents</code><br>
+    Les Identifiants sont identiques à ceux de l'erp<br>
+    Si vous souhaitez vous connecter avec un lecteur réseau windows, vous devez installer un certificat https</small>",
+    'document_enable_dav' => array("label"=>"Activer le WebDav","type"=>"checkbox"),
+    'document_enable_dav_logs' => array("label"=>"Activer les logs WebDav  ","type"=>"checkbox"),
+));
+
+
+
+function document_widget(&$widgets){
+	global $myUser;
+	require_once(__DIR__.SLASH.'..'.SLASH.'dashboard'.SLASH.'DashboardWidget.class.php');
+
+
+	$modelWidget = new DashboardWidget();
+	$modelWidget->model = 'document';
+	$modelWidget->title = 'Documents';
+	
+	$modelWidget->icon = 'fas fa-file';
+	$modelWidget->background = '#A3CB38';
+	$modelWidget->callback = 'init_components';
+	$modelWidget->load = 'action.php?action=document_widget_load';
+	$modelWidget->configure = 'action.php?action=document_widget_configure';
+	$modelWidget->configure_callback = 'document_widget_configure_save';
+	$modelWidget->js = [Plugin::url().'/js/widget.js?v='.time()];
+	$modelWidget->css = [Plugin::url().'/css/widget.css?v=1'.time()];
+	$modelWidget->description = "Affiche un espace document";
+
+	$widgets[] = $modelWidget;
+}
+
+
+//Déclation des assets
+Plugin::addCss("/css/main.css"); 
+Plugin::addCss("/css/document.api.css"); 
+Plugin::addJs("/js/component.js",true); 
+Plugin::addJs("/js/document.api.js"); 
+Plugin::addJs("/js/main.js"); 
+
+//Mapping hook / fonctions
+Plugin::addHook("widget", "document_widget");
+Plugin::addHook("install", "document_install");
+Plugin::addHook("uninstall", "document_uninstall"); 
+Plugin::addHook("section", "document_section");
+Plugin::addHook("menu_main", "document_menu"); 
+Plugin::addHook("page", "document_page");  
+Plugin::addHook("action", "document_action");  
+Plugin::addHook("menu_setting", "document_menu_setting");    
+Plugin::addHook("content_setting", "document_content_setting");
+Plugin::addHook("rewrite", "document_dav_document");
+?>

+ 4 - 0
plugin/document/img/breadcrumb.svg

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="44" width="14" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path d="m0.54879 0.047777 12.195 21.952-12.195 21.951 12.195-21.951z" stroke="#d7d7d7" stroke-linecap="round" stroke-miterlimit="31.2" stroke-width="1.0976" fill="#F00"/>
+</svg>

+ 24 - 0
plugin/document/img/default-image.svg

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512px" height="512px">
+<g>
+	<g>
+		<path d="M446.575,0H65.425C29.349,0,0,29.35,0,65.426v381.149C0,482.65,29.349,512,65.425,512h381.15
+			C482.651,512,512,482.65,512,446.574V65.426C512,29.35,482.651,0,446.575,0z M481.842,446.575
+			c0,19.447-15.821,35.267-35.267,35.267H65.425c-19.447,0-35.268-15.821-35.268-35.267v-55.007l99.255-84.451
+			c3.622-3.082,8.906-3.111,12.562-0.075l62.174,51.628c5.995,4.977,14.795,4.569,20.304-0.946L372.181,209.77
+			c2.67-2.675,5.783-2.935,7.408-2.852c1.62,0.083,4.695,0.661,7.078,3.596l95.176,117.19V446.575z M481.842,279.865l-71.766-88.366
+			c-7.117-8.764-17.666-14.122-28.942-14.701c-11.268-0.57-22.317,3.672-30.294,11.662L212.832,326.681l-51.59-42.839
+			c-14.959-12.422-36.563-12.293-51.373,0.308l-79.712,67.822V65.426c0-19.447,15.821-35.268,35.268-35.268h381.15
+			c19.447,0,35.267,15.821,35.267,35.268V279.865z"  fill="#999999"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M161.174,62.995c-40.095,0-72.713,32.62-72.713,72.713c0,40.094,32.619,72.713,72.713,72.713s72.713-32.619,72.713-72.713
+			S201.269,62.995,161.174,62.995z M161.174,178.264c-23.466,0-42.556-19.091-42.556-42.556c0-23.466,19.09-42.556,42.556-42.556
+			c23.466,0,42.556,19.091,42.556,42.556S184.64,178.264,161.174,178.264z"  fill="#999999"/>
+	</g>
+</g>
+
+</svg>

+ 91 - 0
plugin/document/img/file-types/access.svg

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M282.208,19.67c-3.648-3.008-8.48-4.256-13.152-3.392l-256,48C5.472,65.686,0,72.278,0,79.99v352
+			c0,7.68,5.472,14.304,13.056,15.712l256,48c0.96,0.192,1.984,0.288,2.944,0.288c3.68,0,7.328-1.28,10.208-3.68
+			c3.68-3.04,5.792-7.584,5.792-12.32v-448C288,27.222,285.888,22.71,282.208,19.67z M256,460.694L32,418.71V93.27l224-41.984
+			V460.694z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M223.008,330.518l-64-176c-4.576-12.672-25.44-12.672-30.048,0l-64,176c-3.008,8.32,1.28,17.504,9.568,20.512
+			c8.384,3.072,17.504-1.248,20.512-9.568L144,206.806l48.96,134.656c2.368,6.496,8.512,10.528,15.04,10.528
+			c1.824,0,3.648-0.32,5.44-0.96C221.728,348.022,226.048,338.838,223.008,330.518z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M190.4,271.99H97.6c-8.864,0-16,7.168-16,16c0,8.832,7.168,16,16,16h92.8c8.864,0,16-7.168,16-16
+			C206.4,279.158,199.232,271.99,190.4,271.99z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M352,47.99c-30.528,0-59.456,3.008-83.68,8.736c-8.608,2.016-13.92,10.656-11.872,19.264
+			c1.984,8.608,10.496,13.792,19.232,11.904c21.856-5.184,48.224-7.904,76.32-7.904c84,0,128,23.776,128,32s-44,32-128,32
+			c-28.096,0-54.464-2.72-76.32-7.904c-8.736-2.016-17.248,3.296-19.232,11.904c-2.048,8.608,3.264,17.248,11.872,19.264
+			c24.224,5.728,53.152,8.736,83.68,8.736c77.056,0,160-20.032,160-64S429.056,47.99,352,47.99z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M496,191.99c-8.832,0-16,7.168-16,16c0,8.224-44,32-128,32c-28.096,0-54.464-2.72-76.288-7.904
+			c-8.768-1.952-17.28,3.296-19.264,11.904c-2.048,8.608,3.264,17.248,11.872,19.264c24.224,5.728,53.152,8.736,83.68,8.736
+			c77.056,0,160-20.032,160-64C512,199.158,504.832,191.99,496,191.99z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M496,287.99c-8.832,0-16,7.168-16,16c0,8.224-44,32-128,32c-28.096,0-54.464-2.72-76.288-7.872
+			c-8.768-2.176-17.28,3.264-19.264,11.872c-2.048,8.64,3.264,17.248,11.872,19.264c24.224,5.696,53.152,8.736,83.68,8.736
+			c77.056,0,160-20.032,160-64C512,295.158,504.832,287.99,496,287.99z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M496,383.99c-8.832,0-16,7.168-16,16c0,8.224-44,32-128,32c-28.096,0-54.464-2.72-76.288-7.872
+			c-8.768-2.208-17.28,3.264-19.264,11.872c-2.048,8.64,3.264,17.248,11.872,19.264c24.224,5.696,53.152,8.736,83.68,8.736
+			c77.056,0,160-20.032,160-64C512,391.158,504.832,383.99,496,383.99z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M496,95.99c-8.832,0-16,7.168-16,16v288c0,8.832,7.168,16,16,16c8.832,0,16-7.168,16-16v-288
+			C512,103.158,504.832,95.99,496,95.99z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 21 - 0
plugin/document/img/file-types/camera.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="90.801px" height="75.912px" viewBox="0 0 90.801 75.912" enable-background="new 0 0 90.801 75.912" xml:space="preserve">
+<g>
+	<path d="M45.4,19.407c-12.499,0-22.668,10.168-22.668,22.667c0,12.5,10.169,22.668,22.668,22.668
+		c12.499,0,22.668-10.168,22.668-22.668C68.068,29.575,57.898,19.407,45.4,19.407z M45.4,56.742c-8.088,0-14.668-6.58-14.668-14.668
+		c0-8.086,6.58-14.667,14.668-14.667c8.088,0,14.668,6.58,14.668,14.667C60.068,50.162,53.488,56.742,45.4,56.742z"/>
+	<path d="M49.463,30.919c-2.023-0.887-4.383,0.039-5.268,2.063c-0.885,2.024,0.038,4.383,2.063,5.268
+		c0.03,0.014,3.057,1.444,2.743,3.767c-0.296,2.188,1.239,4.202,3.429,4.498c0.182,0.024,0.361,0.036,0.541,0.036
+		c1.971,0,3.688-1.457,3.959-3.465C57.825,36.45,52.657,32.316,49.463,30.919z"/>
+	<path d="M45.98,45.985c-1.049,0-2.08,0.421-2.83,1.171c-0.74,0.739-1.17,1.77-1.17,2.829c0,1.05,0.43,2.08,1.17,2.818
+		c0.74,0.75,1.78,1.182,2.83,1.182c1.051,0,2.08-0.432,2.83-1.182c0.74-0.738,1.17-1.77,1.17-2.818c0-1.051-0.43-2.09-1.17-2.829
+		C48.063,46.406,47.031,45.985,45.98,45.985z"/>
+	<path d="M86.801,18.3H71.682L67.465,2.941C66.986,1.204,65.409,0,63.607,0H27.193c-1.802,0-3.381,1.204-3.857,2.941L19.12,18.3H4
+		c-2.209,0-4,1.791-4,4v49.612c0,2.209,1.791,4,4,4h82.801c2.209,0,4-1.791,4-4V22.3C90.801,20.091,89.01,18.3,86.801,18.3z
+		 M82.801,67.912H8V26.3h14.17c1.802,0,3.381-1.204,3.857-2.941L30.243,8h30.313l4.217,15.358c0.479,1.737,2.057,2.941,3.857,2.941
+		H82.8L82.801,67.912L82.801,67.912z"/>
+</g>
+</svg>

+ 14 - 0
plugin/document/img/file-types/code.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="83.432px" height="57.859px" viewBox="0 0 83.432 57.859" enable-background="new 0 0 83.432 57.859" xml:space="preserve">
+<g>
+	<path d="M54.5,57.86c-1.151,0-2.305-0.438-3.183-1.317c-1.758-1.758-1.758-4.607,0-6.364L72.567,28.93L51.318,7.682
+		c-1.758-1.757-1.758-4.605,0-6.364c1.757-1.758,4.605-1.757,6.363,0l24.432,24.431c0.845,0.844,1.318,1.988,1.318,3.182
+		c0,1.194-0.475,2.338-1.318,3.182L57.681,56.542C56.804,57.42,55.651,57.86,54.5,57.86z"/>
+	<path d="M28.932,57.86c-1.151,0-2.303-0.438-3.182-1.317L1.318,32.112C0.475,31.268,0,30.124,0,28.93
+		c0-1.193,0.475-2.338,1.317-3.182L25.75,1.318c1.756-1.757,4.606-1.757,6.362,0c1.759,1.758,1.759,4.606,0,6.364L10.864,28.93
+		l21.249,21.248c1.759,1.757,1.759,4.606,0,6.364C31.235,57.42,30.083,57.86,28.932,57.86z"/>
+</g>
+</svg>

+ 15 - 0
plugin/document/img/file-types/contact.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="90.801px" height="84.482px" viewBox="0 0 90.801 84.482" enable-background="new 0 0 90.801 84.482" xml:space="preserve">
+<g>
+	<path d="M45.4,53.197c14.667,0,26.599-11.932,26.599-26.6C71.999,11.932,60.066,0,45.4,0C30.733,0,18.802,11.932,18.802,26.597
+		C18.802,41.265,30.733,53.197,45.4,53.197z M45.4,8c10.254,0,18.599,8.343,18.599,18.598c0,10.257-8.345,18.6-18.599,18.6
+		c-10.255,0-18.599-8.343-18.599-18.6C26.802,16.343,35.146,8,45.4,8z"/>
+	<path d="M57.699,55.638c-1.311-0.416-2.744-0.13-3.793,0.76L45.4,63.591l-8.506-7.192c-1.049-0.89-2.485-1.176-3.793-0.76
+		C27.574,57.392,0,66.833,0,80.482c0,2.209,1.791,4,4,4h82.801c2.209,0,4-1.791,4-4C90.801,66.833,63.227,57.392,57.699,55.638z
+		 M10.165,76.482c4.394-4.521,14.671-9.541,23.277-12.527l9.375,7.93c1.49,1.263,3.676,1.263,5.166,0l9.375-7.93
+		c8.606,2.986,18.885,8.006,23.277,12.527H10.165z"/>
+</g>
+</svg>

+ 19 - 0
plugin/document/img/file-types/document.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="67.867px" height="87.199px" viewBox="0 0 67.867 87.199" enable-background="new 0 0 67.867 87.199" xml:space="preserve">
+<g>
+	<path d="M63.867,0H4C1.791,0,0,1.791,0,4v79.199c0,2.209,1.791,4,4,4h59.867c2.209,0,4-1.791,4-4V4C67.867,1.791,66.076,0,63.867,0
+		z M59.867,79.199H8V8h51.867V79.199z"/>
+	<path d="M20.93,56.768h11.795c1.933,0,3.5-1.568,3.5-3.5c0-1.935-1.567-3.5-3.5-3.5H20.93c-1.933,0-3.5,1.565-3.5,3.5
+		C17.43,55.199,18.997,56.768,20.93,56.768z"/>
+	<path d="M21.484,41.058h23.75c1.934,0,3.5-1.567,3.5-3.5s-1.566-3.5-3.5-3.5h-23.75c-1.933,0-3.5,1.567-3.5,3.5
+		S19.552,41.058,21.484,41.058z"/>
+	<path d="M21.484,25.348h23.75c1.934,0,3.5-1.567,3.5-3.5s-1.566-3.5-3.5-3.5h-23.75c-1.933,0-3.5,1.567-3.5,3.5
+		S19.552,25.348,21.484,25.348z"/>
+	<path d="M42.628,50.794c-0.66,0.649-1.03,1.551-1.03,2.472c0,0.92,0.37,1.817,1.03,2.479c0.648,0.65,1.55,1.021,2.47,1.021
+		s1.819-0.371,2.47-1.021c0.66-0.66,1.03-1.559,1.03-2.479c0-0.921-0.37-1.82-1.03-2.472C46.257,49.484,43.928,49.494,42.628,50.794
+		z"/>
+</g>
+</svg>

+ 13 - 0
plugin/document/img/file-types/download.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="86.818px" height="89.201px" viewBox="0 0 86.818 89.201" enable-background="new 0 0 86.818 89.201" xml:space="preserve">
+<g>
+	<path d="M81.818,52.244c-2.762,0-5,2.237-5,5v21.957H10V57.244c0-2.763-2.237-5-5-5c-2.762,0-5,2.237-5,5v26.957
+		c0,2.762,2.238,5,5,5h76.818c2.762,0,5-2.238,5-5V57.244C86.818,54.481,84.58,52.244,81.818,52.244z"/>
+	<path d="M39.873,63.522c0.938,0.938,2.21,1.465,3.536,1.465s2.598-0.524,3.535-1.465l20.162-20.162c1.953-1.952,1.953-5.118,0-7.07
+		c-1.951-1.953-5.119-1.953-7.07,0L48.409,47.917V5c0-2.762-2.238-5-5-5s-5,2.238-5,5v42.916L26.785,36.291
+		c-1.953-1.953-5.12-1.952-7.071-0.001c-1.952,1.953-1.952,5.119-0.001,7.071L39.873,63.522z"/>
+</g>
+</svg>

+ 109 - 0
plugin/document/img/file-types/excel.svg

@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M282.208,19.67c-3.648-3.008-8.48-4.256-13.152-3.392l-256,48C5.472,65.686,0,72.278,0,79.99v352
+			c0,7.68,5.472,14.304,13.056,15.712l256,48c0.96,0.192,1.984,0.288,2.944,0.288c3.68,0,7.328-1.28,10.208-3.68
+			c3.68-3.04,5.792-7.584,5.792-12.32v-448C288,27.222,285.888,22.71,282.208,19.67z M256,460.694L32,418.71V93.27l224-41.984
+			V460.694z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M496,79.99H272c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h208v288H272c-8.832,0-16,7.168-16,16
+			c0,8.832,7.168,16,16,16h224c8.832,0,16-7.168,16-16v-320C512,87.158,504.832,79.99,496,79.99z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M336,143.99h-64c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h64c8.832,0,16-7.168,16-16
+			C352,151.158,344.832,143.99,336,143.99z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M336,207.99h-64c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h64c8.832,0,16-7.168,16-16
+			C352,215.158,344.832,207.99,336,207.99z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M336,271.99h-64c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h64c8.832,0,16-7.168,16-16
+			C352,279.158,344.832,271.99,336,271.99z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M336,335.99h-64c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h64c8.832,0,16-7.168,16-16
+			C352,343.158,344.832,335.99,336,335.99z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,143.99h-32c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h32c8.832,0,16-7.168,16-16
+			C448,151.158,440.832,143.99,432,143.99z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,207.99h-32c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h32c8.832,0,16-7.168,16-16
+			C448,215.158,440.832,207.99,432,207.99z"  fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,271.99h-32c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h32c8.832,0,16-7.168,16-16
+			C448,279.158,440.832,271.99,432,271.99z"  fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,335.99h-32c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h32c8.832,0,16-7.168,16-16
+			C448,343.158,440.832,335.99,432,335.99z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M220.064,309.462l-112-128c-5.888-6.688-15.968-7.328-22.592-1.504c-6.656,5.824-7.328,15.936-1.504,22.56l112,128
+			c3.168,3.616,7.584,5.472,12.032,5.472c3.744,0,7.488-1.312,10.56-3.968C225.216,326.198,225.888,316.118,220.064,309.462z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M217.824,163.382c-6.976-5.472-17.024-4.16-22.464,2.784l-112,144c-5.408,6.976-4.16,17.056,2.816,22.464
+			c2.944,2.272,6.4,3.36,9.824,3.36c4.736,0,9.472-2.112,12.608-6.144l112-144C226.048,178.838,224.8,168.79,217.824,163.382z" fill="#27ae60"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 18 - 0
plugin/document/img/file-types/feed.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="78.967px" height="79.412px" viewBox="0 0 78.967 79.412" enable-background="new 0 0 78.967 79.412" xml:space="preserve">
+<g>
+	<path d="M41.56,35.625C26.933,21.98,5.779,24.18,4.886,24.281c-3.019,0.339-5.19,3.062-4.852,6.08
+		c0.339,3.018,3.052,5.193,6.08,4.852c0.165-0.019,16.975-1.775,27.94,8.456c6.917,6.453,10.136,16.551,9.564,30.012
+		c-0.129,3.035,2.227,5.601,5.262,5.729c0.08,0.002,0.158,0.004,0.237,0.004c2.93,0,5.364-2.312,5.491-5.267
+		C55.324,57.33,50.933,44.369,41.56,35.625z"/>
+	<path d="M13.504,53.26c-3.42,0-6.77,1.382-9.19,3.802s-3.81,5.77-3.81,9.198c0,3.421,1.39,6.771,3.81,9.189
+		c2.421,2.42,5.771,3.811,9.19,3.811c3.43,0,6.78-1.391,9.19-3.811c2.42-2.421,3.81-5.771,3.81-9.189c0-3.43-1.39-6.778-3.81-9.198
+		C20.285,54.642,16.934,53.26,13.504,53.26z"/>
+	<path d="M59.521,16.75C37.765-3.376,6.176,0.178,4.84,0.34c-3.016,0.364-5.164,3.104-4.8,6.12c0.364,3.014,3.093,5.169,6.12,4.8
+		c0.274-0.031,27.831-3.143,45.891,13.564c11.421,10.565,16.74,27.001,15.813,48.854c-0.129,3.035,2.227,5.6,5.262,5.729
+		c0.079,0.002,0.158,0.004,0.236,0.004c2.932,0,5.365-2.312,5.492-5.267C79.925,48.92,73.42,29.609,59.521,16.75z"/>
+</g>
+</svg>

+ 9 - 0
plugin/document/img/file-types/folder.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="85.875px" height="72.799px" viewBox="0 0 85.875 72.799" enable-background="new 0 0 85.875 72.799" xml:space="preserve">
+<path d="M81.875,72.799H4c-2.209,0-4-1.791-4-4V4c0-2.209,1.791-4,4-4h29.426c1.405,0,2.708,0.737,3.431,1.943l5.168,8.621h39.851
+	c2.209,0,4,1.791,4,4v54.235C85.875,71.008,84.084,72.799,81.875,72.799z M8,64.799h69.875V18.564H39.759
+	c-1.405,0-2.708-0.737-3.431-1.943L31.16,8H8V64.799z" fill="#f1c40f"/>
+</svg>

+ 49 - 0
plugin/document/img/file-types/gear.svg

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="93.4px" height="93.398px" viewBox="0 0 93.4 93.398" enable-background="new 0 0 93.4 93.398" xml:space="preserve">
+<g>
+	<path d="M92.437,36.781c-0.356-1.385-1.52-2.412-2.937-2.599l-7.656-1.002c-0.165-0.444-0.281-0.744-0.297-0.78l-0.13-0.312
+		c0,0-0.119-0.271-0.309-0.683l4.705-6.121c0.871-1.134,0.966-2.683,0.239-3.913c-0.08-0.137-2.017-3.379-6.331-7.697
+		c-4.315-4.314-7.561-6.249-7.695-6.329c-1.231-0.728-2.779-0.634-3.912,0.239l-6.121,4.705c-0.412-0.188-0.681-0.309-0.681-0.309
+		c-0.036-0.017-0.072-0.031-0.109-0.047l-0.286-0.115c0,0-0.273-0.104-0.7-0.265l-1.001-7.657c-0.186-1.417-1.214-2.579-2.598-2.936
+		C56.466,0.925,52.804,0,46.701,0s-9.767,0.925-9.92,0.964C35.397,1.32,34.369,2.483,34.183,3.9l-1.001,7.658
+		c-0.439,0.164-0.734,0.277-0.764,0.289l-0.233,0.096c-0.032,0.014-0.063,0.026-0.096,0.04c0,0-0.27,0.12-0.683,0.31l-6.122-4.706
+		c-1.133-0.871-2.683-0.964-3.912-0.238c-0.136,0.08-3.378,2.015-7.697,6.331c-4.313,4.316-6.25,7.561-6.329,7.695
+		c-0.728,1.23-0.633,2.779,0.238,3.914l4.706,6.119c-0.188,0.412-0.309,0.682-0.309,0.682l0.016,0.007
+		c-0.064,0.122-0.124,0.25-0.175,0.383c0,0-0.107,0.276-0.268,0.704L3.9,34.184c-1.417,0.187-2.579,1.214-2.937,2.599
+		C0.926,36.935,0.001,40.596,0,46.7c0,6.104,0.926,9.766,0.965,9.92c0.357,1.383,1.521,2.41,2.937,2.596l7.656,1.002
+		c0.165,0.441,0.278,0.736,0.29,0.764l0.096,0.234c0.014,0.031,0.027,0.064,0.041,0.096c0,0,0.119,0.271,0.309,0.683l-4.706,6.121
+		c-0.872,1.133-0.966,2.684-0.238,3.913c0.08,0.136,2.017,3.378,6.33,7.693c4.315,4.318,7.563,6.252,7.697,6.332
+		c1.229,0.728,2.778,0.631,3.911-0.24l6.121-4.705c0.414,0.189,0.684,0.31,0.684,0.31c0.031,0.014,0.063,0.026,0.096,0.041
+		l0.295,0.119c0,0,0.274,0.106,0.702,0.266l1.002,7.658c0.186,1.416,1.215,2.578,2.599,2.936c0.153,0.039,3.813,0.963,9.918,0.963
+		c6.104,0,9.765-0.924,9.918-0.963c1.384-0.356,2.412-1.52,2.598-2.936l1.002-7.658c0.441-0.164,0.74-0.279,0.774-0.293l0.318-0.133
+		c0,0,0.27-0.119,0.683-0.31l6.12,4.705c1.135,0.873,2.686,0.968,3.914,0.238c0.137-0.08,3.377-2.016,7.695-6.33
+		c4.313-4.315,6.25-7.559,6.33-7.693c0.727-1.229,0.633-2.78-0.238-3.913l-4.706-6.121c0.188-0.414,0.309-0.685,0.309-0.685
+		c0.015-0.031,0.027-0.063,0.04-0.094l0.12-0.295c0,0,0.106-0.275,0.267-0.703l7.656-1.002c1.417-0.186,2.579-1.213,2.937-2.598
+		c0.039-0.152,0.963-3.813,0.964-9.918C93.4,40.597,92.476,36.935,92.437,36.781z M86.043,52.608l-7.226,0.945
+		c-1.321,0.172-2.43,1.08-2.859,2.342c-0.413,1.215-0.886,2.439-0.886,2.439l-0.074,0.184c-0.104,0.232-0.592,1.32-1.105,2.363
+		c-0.589,1.197-0.448,2.625,0.363,3.682l4.441,5.777c-0.859,1.117-2.151,2.658-3.924,4.43c-1.774,1.773-3.316,3.066-4.433,3.926
+		l-5.775-4.44c-1.06-0.813-2.484-0.953-3.683-0.365c-1.142,0.564-2.331,1.094-2.331,1.094s0.004,0,0.005-0.002l-0.157,0.066
+		c0,0-1.26,0.486-2.505,0.91c-1.262,0.43-2.168,1.539-2.341,2.858l-0.944,7.226c-1.397,0.182-3.4,0.356-5.907,0.356
+		c-2.51,0-4.514-0.176-5.91-0.356l-0.944-7.226c-0.174-1.321-1.081-2.43-2.343-2.858c-1.213-0.414-2.438-0.885-2.438-0.885
+		l-0.187-0.076c-0.231-0.104-1.317-0.59-2.36-1.105c-1.193-0.588-2.624-0.449-3.684,0.363l-5.778,4.443
+		c-1.116-0.859-2.658-2.152-4.432-3.927c-1.772-1.772-3.064-3.315-3.925-4.43l4.442-5.779c0.813-1.059,0.955-2.485,0.363-3.684
+		c-0.52-1.051-1.011-2.146-1.109-2.367l-0.048-0.117c-0.01-0.021-0.488-1.264-0.906-2.496c-0.428-1.264-1.538-2.174-2.86-2.346
+		l-7.228-0.945C7.177,51.211,7,49.208,7,46.702c0-2.511,0.177-4.515,0.358-5.911l7.227-0.945c1.322-0.174,2.433-1.083,2.86-2.346
+		c0.417-1.23,0.897-2.476,0.906-2.495c0.011-0.026,0.02-0.052,0.028-0.077h0.001c0,0,0.548-1.23,1.128-2.408
+		c0.592-1.196,0.449-2.625-0.363-3.684l-4.442-5.777c0.859-1.116,2.151-2.658,3.923-4.432c1.775-1.773,3.317-3.065,4.434-3.925
+		l5.778,4.441c1.059,0.813,2.484,0.951,3.682,0.364c1.054-0.521,2.15-1.012,2.37-1.109l0.112-0.047
+		c0.021-0.008,1.271-0.489,2.504-0.91c1.262-0.43,2.168-1.538,2.342-2.857l0.943-7.228C42.188,7.177,44.193,7,46.701,7
+		s4.512,0.177,5.908,0.356l0.944,7.228c0.173,1.319,1.08,2.429,2.342,2.857c1.219,0.416,2.45,0.891,2.45,0.891h0.001l0.177,0.07
+		c0.245,0.109,1.324,0.596,2.361,1.106c1.194,0.588,2.622,0.447,3.68-0.366l5.777-4.44c1.116,0.858,2.657,2.15,4.431,3.923
+		c1.772,1.774,3.065,3.317,3.926,4.434l-4.441,5.778c-0.813,1.057-0.953,2.483-0.364,3.681c0.563,1.141,1.093,2.332,1.093,2.332
+		v-0.002L75.049,35c0.009,0.021,0.489,1.27,0.91,2.504c0.431,1.261,1.537,2.169,2.858,2.342l7.226,0.945
+		c0.182,1.396,0.357,3.4,0.357,5.909C86.4,49.208,86.224,51.211,86.043,52.608z"/>
+	<path d="M46.701,24.401c-12.296,0-22.299,10.003-22.299,22.299c0,12.295,10.003,22.299,22.299,22.299
+		c12.295,0,22.297-10.004,22.297-22.299C68.998,34.404,58.996,24.401,46.701,24.401z M46.701,61.999
+		c-8.436,0-15.299-6.863-15.299-15.299s6.863-15.299,15.299-15.299S61.998,38.265,61.998,46.7
+		C61.998,55.135,55.136,61.999,46.701,61.999z"/>
+</g>
+</svg>

+ 24 - 0
plugin/document/img/file-types/image.svg

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512px" height="512px">
+<g>
+	<g>
+		<path d="M446.575,0H65.425C29.349,0,0,29.35,0,65.426v381.149C0,482.65,29.349,512,65.425,512h381.15
+			C482.651,512,512,482.65,512,446.574V65.426C512,29.35,482.651,0,446.575,0z M481.842,446.575
+			c0,19.447-15.821,35.267-35.267,35.267H65.425c-19.447,0-35.268-15.821-35.268-35.267v-55.007l99.255-84.451
+			c3.622-3.082,8.906-3.111,12.562-0.075l62.174,51.628c5.995,4.977,14.795,4.569,20.304-0.946L372.181,209.77
+			c2.67-2.675,5.783-2.935,7.408-2.852c1.62,0.083,4.695,0.661,7.078,3.596l95.176,117.19V446.575z M481.842,279.865l-71.766-88.366
+			c-7.117-8.764-17.666-14.122-28.942-14.701c-11.268-0.57-22.317,3.672-30.294,11.662L212.832,326.681l-51.59-42.839
+			c-14.959-12.422-36.563-12.293-51.373,0.308l-79.712,67.822V65.426c0-19.447,15.821-35.268,35.268-35.268h381.15
+			c19.447,0,35.267,15.821,35.267,35.268V279.865z"  fill="#8e44ad"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M161.174,62.995c-40.095,0-72.713,32.62-72.713,72.713c0,40.094,32.619,72.713,72.713,72.713s72.713-32.619,72.713-72.713
+			S201.269,62.995,161.174,62.995z M161.174,178.264c-23.466,0-42.556-19.091-42.556-42.556c0-23.466,19.09-42.556,42.556-42.556
+			c23.466,0,42.556,19.091,42.556,42.556S184.64,178.264,161.174,178.264z"  fill="#8e44ad"/>
+	</g>
+</g>
+
+</svg>

+ 18 - 0
plugin/document/img/file-types/information.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="87.199px" height="87.2px" viewBox="0 0 87.199 87.2" enable-background="new 0 0 87.199 87.2" xml:space="preserve">
+<g>
+	<path d="M49.438,59.241l-3.685,1.605l4.793-24.332c0.265-1.341-0.274-2.712-1.383-3.513c-1.106-0.8-2.578-0.882-3.769-0.214
+		l-8.88,5.002c-1.685,0.949-2.28,3.083-1.332,4.768c0.951,1.686,3.084,2.281,4.768,1.332l2.26-1.273l-4.615,23.429
+		c-0.252,1.277,0.226,2.59,1.24,3.404c0.633,0.51,1.409,0.772,2.194,0.772c0.473,0,0.948-0.097,1.396-0.291l9.808-4.272
+		c1.771-0.771,2.582-2.834,1.812-4.605C53.271,59.28,51.209,58.467,49.438,59.241z"/>
+	<path d="M47.113,26.254c1.32,0,2.601-0.53,3.531-1.46c0.93-0.93,1.469-2.22,1.469-3.54c0-1.32-0.539-2.61-1.469-3.53
+		c-0.932-0.93-2.222-1.47-3.531-1.47c-1.319,0-2.609,0.54-3.539,1.47c-0.93,0.92-1.46,2.21-1.46,3.53c0,1.32,0.53,2.61,1.46,3.54
+		C44.504,25.724,45.794,26.254,47.113,26.254z"/>
+	<path d="M43.6,0C19.559,0,0,19.559,0,43.6s19.559,43.6,43.6,43.6c24.041,0,43.6-19.559,43.6-43.6C87.199,19.559,67.641,0,43.6,0z
+		 M43.6,79.2C23.97,79.2,8,63.229,8,43.6C8,23.97,23.97,8,43.6,8c19.63,0,35.6,15.97,35.6,35.6C79.199,63.229,63.229,79.2,43.6,79.2
+		z"/>
+</g>
+</svg>

+ 20 - 0
plugin/document/img/file-types/mail.svg

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="94.398px" height="54.766px" viewBox="0 0 94.398 54.766" enable-background="new 0 0 94.398 54.766" xml:space="preserve">
+<path d="M94.392,3.926c-0.002-0.153-0.024-0.308-0.046-0.461c-0.015-0.108-0.021-0.221-0.045-0.326
+	c-0.028-0.131-0.077-0.257-0.119-0.385c-0.04-0.123-0.074-0.248-0.126-0.364c-0.046-0.104-0.109-0.201-0.164-0.303
+	c-0.072-0.132-0.142-0.266-0.229-0.388c-0.016-0.021-0.023-0.045-0.039-0.066C93.568,1.558,93.5,1.499,93.44,1.429
+	c-0.094-0.11-0.186-0.223-0.291-0.321c-0.093-0.089-0.192-0.165-0.294-0.243c-0.104-0.081-0.204-0.162-0.314-0.233
+	c-0.109-0.068-0.226-0.126-0.339-0.184c-0.116-0.06-0.231-0.119-0.354-0.167c-0.121-0.047-0.246-0.08-0.371-0.115
+	c-0.123-0.034-0.245-0.071-0.373-0.094c-0.144-0.026-0.288-0.034-0.433-0.044C90.579,0.021,90.492,0,90.399,0H4
+	C3.908,0,3.823,0.021,3.733,0.027c-0.146,0.01-0.294,0.018-0.438,0.044C3.169,0.094,3.051,0.13,2.93,0.163
+	c-0.129,0.036-0.257,0.07-0.381,0.118c-0.118,0.047-0.23,0.104-0.342,0.161C2.088,0.502,1.97,0.56,1.856,0.632
+	C1.749,0.701,1.651,0.78,1.552,0.857c-0.104,0.081-0.208,0.16-0.305,0.252C1.144,1.208,1.054,1.317,0.962,1.426
+	C0.901,1.497,0.831,1.557,0.775,1.633C0.76,1.654,0.751,1.677,0.736,1.699C0.649,1.821,0.581,1.954,0.508,2.086
+	C0.452,2.188,0.39,2.285,0.344,2.389C0.292,2.506,0.259,2.631,0.218,2.753c-0.042,0.128-0.09,0.255-0.119,0.386
+	c-0.023,0.105-0.03,0.217-0.045,0.326C0.033,3.618,0.011,3.771,0.008,3.926C0.007,3.951,0,3.975,0,4v46.766c0,2.209,1.791,4,4,4
+	h86.398c2.209,0,4-1.791,4-4V4C94.399,3.975,94.393,3.951,94.392,3.926z M78.188,8L47.199,30.744L16.211,8H78.188z M8,46.765V11.897
+	l36.832,27.034c0.705,0.516,1.536,0.774,2.367,0.774c0.83,0,1.661-0.26,2.366-0.774l36.833-27.034v34.868H8z"/>
+</svg>

+ 16 - 0
plugin/document/img/file-types/message.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="90.801px" height="78.629px" viewBox="0 0 90.801 78.629" enable-background="new 0 0 90.801 78.629" xml:space="preserve">
+<g>
+	<path d="M86.801,0H4C1.791,0,0,1.791,0,4v50.094c0,2.21,1.791,4,4,4h46.704v16.535c0,1.677,1.045,3.177,2.618,3.754
+		c0.451,0.166,0.918,0.246,1.381,0.246c1.15,0,2.271-0.496,3.046-1.405l16.29-19.13h12.762c2.209,0,4-1.79,4-4V4
+		C90.801,1.791,89.01,0,86.801,0z M82.801,50.094H72.191c-1.172,0-2.285,0.517-3.045,1.406L58.704,63.763v-9.669
+		c0-2.209-1.791-4-4-4H8V8h74.801V50.094z"/>
+	<path d="M22.372,25.891h28.544c2.209,0,4-1.791,4-4s-1.791-4-4-4H22.372c-2.209,0-4,1.791-4,4S20.163,25.891,22.372,25.891z"/>
+	<path d="M64.488,25.888c1.051,0,2.091-0.42,2.83-1.17c0.75-0.74,1.181-1.771,1.181-2.83c0-1.05-0.431-2.08-1.181-2.83
+		c-0.739-0.74-1.77-1.17-2.83-1.17c-1.049,0-2.079,0.43-2.818,1.17c-0.75,0.75-1.171,1.78-1.171,2.83c0,1.06,0.421,2.09,1.171,2.83
+		C62.409,25.468,63.449,25.888,64.488,25.888z"/>
+</g>
+</svg>

+ 23 - 0
plugin/document/img/file-types/microphone.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="55.182px" height="101.1px" viewBox="0 0 55.182 101.1" enable-background="new 0 0 55.182 101.1" xml:space="preserve">
+<g>
+	<path d="M20.705,67.677l13.501,0.104c0.01,0,0.046,0,0.056,0c4.827,0,9.601-4.729,9.637-9.546l0.371-48.44
+		c0.036-4.763-4.781-9.655-9.546-9.692L21.168,0c-4.827,0-9.601,4.728-9.636,9.545l-0.371,48.438
+		c-0.019,2.289,1.06,4.707,2.957,6.636C16.016,66.545,18.417,67.66,20.705,67.677z M19.529,9.666c0.099-0.55,1.145-1.58,1.636-1.666
+		l13.438,0.099c0.551,0.099,1.581,1.146,1.668,1.635l-0.367,48.381c-0.098,0.544-1.122,1.557-1.678,1.667L20.828,59.68
+		c-0.55-0.098-1.578-1.143-1.665-1.635L19.529,9.666z"/>
+	<path d="M48.463,94.1l-17.71-0.136l0.076-9.879c0.015-1.934-1.54-3.512-3.475-3.526c-0.009,0-0.018,0-0.026,0
+		c-1.92,0-3.484,1.55-3.499,3.475l-0.076,9.878L6.044,93.775c-0.009,0-0.019,0-0.027,0c-1.92,0-3.483,1.55-3.499,3.474
+		c-0.015,1.934,1.54,3.512,3.475,3.526l21.209,0.162l0,0l0,0l21.21,0.163c0.009,0,0.018,0,0.026,0c1.92,0,3.484-1.55,3.499-3.474
+		C51.952,95.694,50.397,94.115,48.463,94.1z"/>
+	<path d="M51.708,31.843c-0.009,0-0.019,0-0.027,0c-1.92,0-3.484,1.549-3.499,3.473l-0.229,29.792
+		c-0.014,1.728-0.698,3.347-1.93,4.558c-1.229,1.212-2.854,1.865-4.585,1.857L13.415,71.31c-1.728-0.014-3.346-0.699-4.558-1.93
+		C7.647,68.15,6.987,66.522,7,64.795l0.228-29.793c0.016-1.934-1.54-3.512-3.474-3.527c-0.009,0-0.019,0-0.027,0
+		c-1.92,0-3.483,1.549-3.499,3.474L0,64.742c-0.028,3.596,1.347,6.987,3.87,9.551c2.522,2.563,5.894,3.989,9.489,4.018l28.025,0.214
+		c0.036,0,0.071,0.001,0.107,0.001c3.556,0,6.905-1.373,9.442-3.871c2.563-2.523,3.989-5.895,4.018-9.489l0.229-29.793
+		C55.197,33.437,53.642,31.858,51.708,31.843z"/>
+</g>
+</svg>

+ 16 - 0
plugin/document/img/file-types/music.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="89.734px" height="90.8px" viewBox="0 0 89.734 90.8" enable-background="new 0 0 89.734 90.8" xml:space="preserve">
+<path d="M89.735,4.03c0.01-1.213-0.531-2.365-1.473-3.131s-2.178-1.064-3.363-0.812L32.292,11.331
+	c-1.834,0.392-3.149,2.005-3.164,3.881l-0.362,47.11c-2.932-1.744-6.48-2.783-10.313-2.813l-0.17-0.002
+	C8.267,59.507,0.066,66.464,0,75.015c-0.065,8.625,8.104,15.706,18.212,15.783L18.38,90.8c10.018,0,18.221-6.955,18.287-15.506l0,0
+	l0,0l0.314-41.065l44.595-9.53l-0.203,26.38c-2.933-1.744-6.482-2.785-10.314-2.813l-0.17-0.001
+	c-10.017,0-18.219,6.956-18.284,15.506c-0.064,8.627,8.105,15.708,18.214,15.786l0.167,0.001c9.237,0,16.916-5.918,18.112-13.543
+	c0.098-0.342,0.167-0.693,0.17-1.066L89.735,4.03z M18.38,82.8l-0.106-0.002C12.673,82.755,7.969,79.218,8,75.076
+	c0.03-4.173,4.645-7.566,10.282-7.566h0.107c5.603,0.043,10.309,3.58,10.276,7.726C28.635,79.406,24.02,82.8,18.38,82.8z
+	 M37.03,28.084l0.074-9.601l44.593-9.53l-0.072,9.602L37.03,28.084z M70.986,71.558l-0.104-0.001
+	c-5.603-0.043-10.307-3.581-10.275-7.726c0.031-4.172,4.646-7.566,10.284-7.566l0.108,0.001c5.603,0.043,10.309,3.581,10.276,7.726
+	C81.243,68.164,76.627,71.558,70.986,71.558z"/>
+</svg>

+ 62 - 0
plugin/document/img/file-types/outlook.svg

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M282.208,19.67c-3.648-3.008-8.48-4.256-13.152-3.392l-256,48C5.472,65.686,0,72.278,0,79.99v352
+			c0,7.68,5.472,14.304,13.056,15.712l256,48c0.96,0.192,1.984,0.288,2.944,0.288c3.68,0,7.328-1.28,10.208-3.68
+			c3.68-3.04,5.792-7.584,5.792-12.32v-448C288,27.222,285.888,22.71,282.208,19.67z M256,460.694L32,418.71V93.27l224-41.984
+			V460.694z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M144,175.99c-44.096,0-80,43.072-80,96s35.904,96,80,96s80-43.072,80-96S188.096,175.99,144,175.99z M144,335.99
+			c-26.464,0-48-28.704-48-64c0-35.296,21.536-64,48-64s48,28.704,48,64S170.464,335.99,144,335.99z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M496,111.99H272c-8.832,0-16,7.168-16,16s7.168,16,16,16h208v224H272c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h224
+			c8.832,0,16-7.168,16-16v-256C512,119.158,504.832,111.99,496,111.99z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M508.608,118.134c-5.472-6.976-15.52-8.256-22.432-2.784L351.04,220.438l-70.464-44.832
+			c-7.392-4.704-17.312-2.528-22.08,4.928c-4.736,7.456-2.528,17.344,4.896,22.08l80,50.88c2.624,1.664,5.632,2.496,8.608,2.496
+			c3.456,0,6.944-1.12,9.792-3.392l144-112C512.8,135.158,514.048,125.11,508.608,118.134z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 70 - 0
plugin/document/img/file-types/pdf.svg

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
+<g>
+	<path d="M50.95,12.187l-0.771-0.771L40.084,1.321L39.313,0.55C38.964,0.201,38.48,0,37.985,0H8.963C7.777,0,6.5,0.916,6.5,2.926V39
+		v16.537V56c0,0.837,0.842,1.653,1.838,1.91c0.05,0.013,0.098,0.032,0.15,0.042C8.644,57.983,8.803,58,8.963,58h40.074
+		c0.16,0,0.319-0.017,0.475-0.048c0.052-0.01,0.1-0.029,0.15-0.042C50.658,57.653,51.5,56.837,51.5,56v-0.463V39V13.978
+		C51.5,13.211,51.408,12.645,50.95,12.187z M47.935,12H39.5V3.565L47.935,12z M8.963,56c-0.071,0-0.135-0.026-0.198-0.049
+		C8.609,55.877,8.5,55.721,8.5,55.537V41h41v14.537c0,0.184-0.109,0.339-0.265,0.414C49.172,55.974,49.108,56,49.037,56H8.963z
+		 M8.5,39V2.926C8.5,2.709,8.533,2,8.963,2h28.595C37.525,2.126,37.5,2.256,37.5,2.391V14h11.609c0.135,0,0.264-0.025,0.39-0.058
+		c0,0.015,0.001,0.021,0.001,0.036V39H8.5z" fill="#e74c3c"/>
+	<path d="M22.042,44.744c-0.333-0.273-0.709-0.479-1.128-0.615c-0.419-0.137-0.843-0.205-1.271-0.205h-2.898V54h1.641v-3.637h1.217
+		c0.528,0,1.012-0.077,1.449-0.232s0.811-0.374,1.121-0.656c0.31-0.282,0.551-0.631,0.725-1.046c0.173-0.415,0.26-0.877,0.26-1.388
+		c0-0.483-0.103-0.918-0.308-1.306S22.375,45.018,22.042,44.744z M21.42,48.073c-0.101,0.278-0.232,0.494-0.396,0.649
+		c-0.164,0.155-0.344,0.267-0.54,0.335c-0.196,0.068-0.395,0.103-0.595,0.103h-1.504v-3.992h1.23c0.419,0,0.756,0.066,1.012,0.198
+		c0.255,0.132,0.453,0.296,0.595,0.492c0.141,0.196,0.234,0.401,0.28,0.615c0.045,0.214,0.068,0.403,0.068,0.567
+		C21.57,47.451,21.52,47.795,21.42,48.073z" fill="#e74c3c"/>
+	<path d="M31.954,45.4c-0.424-0.446-0.957-0.805-1.6-1.073s-1.388-0.403-2.235-0.403h-3.035V54h3.814
+		c0.127,0,0.323-0.016,0.588-0.048c0.264-0.032,0.556-0.104,0.875-0.219c0.319-0.114,0.649-0.285,0.991-0.513
+		s0.649-0.54,0.923-0.937s0.499-0.889,0.677-1.477s0.267-1.297,0.267-2.126c0-0.602-0.105-1.188-0.314-1.757
+		C32.694,46.355,32.378,45.847,31.954,45.4z M30.758,51.73c-0.492,0.711-1.294,1.066-2.406,1.066h-1.627v-7.629h0.957
+		c0.784,0,1.422,0.103,1.914,0.308s0.882,0.474,1.169,0.807s0.48,0.704,0.581,1.114c0.1,0.41,0.15,0.825,0.15,1.244
+		C31.496,49.989,31.25,51.02,30.758,51.73z" fill="#e74c3c"/>
+	<polygon points="35.598,54 37.266,54 37.266,49.461 41.477,49.461 41.477,48.34 37.266,48.34 37.266,45.168 41.9,45.168 
+		41.9,43.924 35.598,43.924 	" fill="#e74c3c"/>
+	<path d="M38.428,22.961c-0.919,0-2.047,0.12-3.358,0.358c-1.83-1.942-3.74-4.778-5.088-7.562c1.337-5.629,0.668-6.426,0.373-6.802
+		c-0.314-0.4-0.757-1.049-1.261-1.049c-0.211,0-0.787,0.096-1.016,0.172c-0.576,0.192-0.886,0.636-1.134,1.215
+		c-0.707,1.653,0.263,4.471,1.261,6.643c-0.853,3.393-2.284,7.454-3.788,10.75c-3.79,1.736-5.803,3.441-5.985,5.068
+		c-0.066,0.592,0.074,1.461,1.115,2.242c0.285,0.213,0.619,0.326,0.967,0.326h0c0.875,0,1.759-0.67,2.782-2.107
+		c0.746-1.048,1.547-2.477,2.383-4.251c2.678-1.171,5.991-2.229,8.828-2.822c1.58,1.517,2.995,2.285,4.211,2.285
+		c0.896,0,1.664-0.412,2.22-1.191c0.579-0.811,0.711-1.537,0.39-2.16C40.943,23.327,39.994,22.961,38.428,22.961z M20.536,32.634
+		c-0.468-0.359-0.441-0.601-0.431-0.692c0.062-0.556,0.933-1.543,3.07-2.744C21.555,32.19,20.685,32.587,20.536,32.634z
+		 M28.736,9.712c0.043-0.014,1.045,1.101,0.096,3.216C27.406,11.469,28.638,9.745,28.736,9.712z M26.669,25.738
+		c1.015-2.419,1.959-5.09,2.674-7.564c1.123,2.018,2.472,3.976,3.822,5.544C31.031,24.219,28.759,24.926,26.669,25.738z
+		 M39.57,25.259C39.262,25.69,38.594,25.7,38.36,25.7c-0.533,0-0.732-0.317-1.547-0.944c0.672-0.086,1.306-0.108,1.811-0.108
+		c0.889,0,1.052,0.131,1.175,0.197C39.777,24.916,39.719,25.05,39.57,25.259z" fill="#e74c3c"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 17 - 0
plugin/document/img/file-types/phone.svg

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="87.204px" height="87.201px" viewBox="0 0 87.204 87.201" enable-background="new 0 0 87.204 87.201" xml:space="preserve">
+<path d="M84.741,11.134c-0.066-0.065-0.134-0.128-0.203-0.188L74.159,1.908C72.862,0.677,71.11,0,69.206,0
+	c-2.178,0-4.289,0.863-5.791,2.368c-0.107,0.108-0.209,0.223-0.305,0.342L52.917,15.577c-2.709,2.913-2.646,7.489,0.188,10.326
+	c0.188,0.188,0.395,0.356,0.613,0.504l5.135,3.416c-0.475,1.854-3.251,7.071-12.598,16.417c-9.272,9.272-14.543,12.088-16.423,12.59
+	l-3.409-5.125c-0.146-0.222-0.315-0.427-0.503-0.613c-1.406-1.403-3.275-2.18-5.263-2.18c-1.896,0-3.685,0.705-5.066,1.989
+	L2.725,63.093c-0.122,0.098-0.237,0.199-0.347,0.311c-2.991,2.994-3.182,7.871-0.453,10.74l9.034,10.379
+	c0.062,0.07,0.124,0.14,0.189,0.203c1.643,1.644,4.16,2.476,7.482,2.476c0.001,0,0.001,0,0.001,0
+	c10.59,0,29.428-8.119,44.946-23.638c8.885-8.887,16.084-19.641,20.271-30.284C85.804,28.31,89.806,16.2,84.741,11.134z
+	 M57.92,57.908C43.233,72.593,26.104,79.201,18.632,79.201c-1.128,0-1.69-0.152-1.898-0.229l-8.64-9.926l12.396-9.818l3.308,4.974
+	c0.068,0.104,0.143,0.202,0.221,0.301c1.26,1.558,3.136,2.416,5.282,2.416h0.001c5.037,0,12.646-5.056,22.611-15.021
+	C62.136,41.674,67.185,33.924,66.92,28.863c-0.104-1.962-0.957-3.688-2.404-4.859c-0.098-0.079-0.197-0.152-0.301-0.222
+	l-4.973-3.309l9.819-12.395l9.937,8.651C80.564,20.219,75.604,40.223,57.92,57.908z"/>
+</svg>

+ 56 - 0
plugin/document/img/file-types/photoshop.svg

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M480,0H32C14.368,0,0,14.368,0,32v448c0,17.664,14.368,32,32,32h448c17.664,0,32-14.336,32-32V32
+			C512,14.368,497.664,0,480,0z M480,480H32V32h448V480z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M352,288c-17.664,0-32-14.336-32-32c0-17.632,14.336-32,32-32c8.544,0,16.576,3.328,22.592,9.376
+			c6.24,6.208,16.384,6.24,22.624-0.032c6.24-6.24,6.24-16.384,0-22.624C385.12,198.656,369.056,192,352,192
+			c-35.296,0-64,28.704-64,64c0,35.296,28.704,64,64,64c17.664,0,32,14.336,32,32s-14.336,32-32,32
+			c-8.544,0-16.544-3.328-22.592-9.376c-6.24-6.24-16.384-6.24-22.624,0s-6.24,16.384,0,22.624C318.88,409.344,334.944,416,352,416
+			c35.296,0,64-28.704,64-64C416,316.704,387.296,288,352,288z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M192,64h-80c-8.832,0-16,7.168-16,16v352c0,8.832,7.168,16,16,16c8.832,0,16-7.168,16-16V256h64c52.928,0,96-43.072,96-96
+			C288,107.072,244.928,64,192,64z M192,224h-64V96h64c35.296,0,64,28.704,64,64C256,195.296,227.296,224,192,224z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 79 - 0
plugin/document/img/file-types/powerpoint.svg

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M282.208,19.67c-3.648-3.008-8.48-4.256-13.152-3.392l-256,48C5.472,65.686,0,72.278,0,79.99v352
+			c0,7.68,5.472,14.304,13.056,15.712l256,48c0.96,0.192,1.984,0.288,2.944,0.288c3.68,0,7.328-1.28,10.208-3.68
+			c3.68-3.04,5.792-7.584,5.792-12.32v-448C288,27.222,285.888,22.71,282.208,19.67z M256,460.694L32,418.71V93.27l224-41.984
+			V460.694z" fill="#e67e22"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M496,79.99H272c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h208v288H272c-8.832,0-16,7.168-16,16
+			c0,8.832,7.168,16,16,16h224c8.832,0,16-7.168,16-16v-320C512,87.158,504.832,79.99,496,79.99z" fill="#e67e22"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,335.99H272c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h160c8.832,0,16-7.168,16-16
+			C448,343.158,440.832,335.99,432,335.99z" fill="#e67e22"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M112,175.99c-8.832,0-16,7.168-16,16v160c0,8.832,7.168,16,16,16c8.832,0,16-7.168,16-16v-160
+			C128,183.158,120.832,175.99,112,175.99z" fill="#e67e22"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M160,175.99h-48c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h48c17.632,0,32,14.368,32,32s-14.368,32-32,32h-48
+			c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h48c35.296,0,64-28.704,64-64C224,204.694,195.296,175.99,160,175.99z" fill="#e67e22"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M368,143.99c-44.128,0-80,35.904-80,80s35.872,80,80,80s80-35.904,80-80S412.128,143.99,368,143.99z M368,271.99
+			c-26.464,0-48-21.536-48-48s21.536-48,48-48s48,21.536,48,48S394.464,271.99,368,271.99z" fill="#e67e22"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,207.99h-48v-48c0-8.832-7.168-16-16-16c-8.832,0-16,7.168-16,16v64c0,8.832,7.168,16,16,16h64
+			c8.832,0,16-7.168,16-16C448,215.158,440.832,207.99,432,207.99z" fill="#e67e22"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 12 - 0
plugin/document/img/file-types/stats.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="93.803px" height="74.157px" viewBox="0 0 93.803 74.157" enable-background="new 0 0 93.803 74.157" xml:space="preserve">
+<path d="M5.496,74.157c-0.972,0-1.952-0.258-2.844-0.797c-2.599-1.572-3.431-4.953-1.856-7.553l12.609-20.832
+	c0.95-1.57,2.621-2.564,4.455-2.646c1.812-0.084,3.586,0.752,4.678,2.23l5.12,6.943l11.199-18.742
+	c0.938-1.569,2.594-2.57,4.418-2.67c1.847-0.099,3.582,0.713,4.687,2.168l9.711,12.807L83.609,2.633
+	c1.584-2.593,4.971-3.41,7.562-1.824c2.592,1.584,3.409,4.969,1.824,7.561L62.854,57.683c-0.946,1.553-2.602,2.534-4.417,2.625
+	c-1.797,0.094-3.56-0.722-4.658-2.17L44.12,45.399L32.858,64.241c-0.943,1.58-2.615,2.584-4.455,2.672
+	c-1.817,0.104-3.599-0.746-4.691-2.229L18.562,57.7l-8.354,13.803C9.172,73.211,7.356,74.157,5.496,74.157z"/>
+</svg>

+ 21 - 0
plugin/document/img/file-types/url.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="97.986px" height="67.465px" viewBox="0 0 97.986 67.465" enable-background="new 0 0 97.986 67.465" xml:space="preserve">
+<g>
+	<path d="M82.324,26.716c0.283-4.615-0.907-8.795-3.482-12.043c-2.979-3.758-7.536-5.912-12.504-5.912
+		c-3.115,0-6.17,0.848-8.917,2.439C52.298,4.027,45.078,0,37.084,0c-7.899,0-15.496,4.039-19.822,10.54
+		c-3.492,5.247-4.513,11.367-2.996,17.57C2.251,33.365-0.913,43.35,0.211,50.82c1.44,9.578,9.213,16.531,18.47,16.531
+		c0.354,0,35.826,0.113,57.316,0.113c14.135,0,21.28-9.922,21.938-19.143C98.539,39.861,93.839,30.052,82.324,26.716z
+		 M89.956,47.754c-0.416,5.828-4.894,11.711-13.958,11.711c-21.478,0-56.937-0.113-57.305-0.113c-5.98,0-9.845-4.891-10.571-9.721
+		c-1.034-6.879,3.571-12.594,12.319-15.285c1.079-0.332,1.967-1.104,2.445-2.127c0.479-1.021,0.504-2.199,0.067-3.24
+		c-2.08-4.971-1.737-9.944,0.966-14.006C26.784,10.672,31.828,8,37.084,8c6.396,0,11.976,3.872,15.707,10.902
+		c0.591,1.112,1.672,1.882,2.917,2.076c1.241,0.193,2.508-0.209,3.41-1.089c2.067-2.019,4.631-3.129,7.219-3.129
+		c2.511,0,4.783,1.051,6.235,2.883c1.785,2.251,2.255,5.46,1.36,9.281c-0.257,1.093-0.041,2.241,0.592,3.168
+		c0.632,0.927,1.625,1.544,2.734,1.705C86.622,35.139,90.382,41.773,89.956,47.754z"/>
+	<path d="M35.656,12.015c-8.231,0.726-10.744,7.292-10.425,11.528c0.139,1.838,1.673,3.236,3.485,3.236
+		c0.09,0,0.179-0.003,0.269-0.01c1.927-0.146,3.372-1.826,3.227-3.754c-0.063-0.855,0.037-3.674,4.061-4.028
+		c1.925-0.17,3.349-1.868,3.179-3.794C39.281,13.269,37.583,11.835,35.656,12.015z"/>
+</g>
+</svg>

+ 82 - 0
plugin/document/img/file-types/word.svg

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M282.208,19.67c-3.648-3.008-8.48-4.256-13.152-3.392l-256,48C5.472,65.686,0,72.278,0,79.99v352
+			c0,7.68,5.472,14.304,13.056,15.712l256,48c0.96,0.192,1.984,0.288,2.944,0.288c3.68,0,7.328-1.28,10.208-3.68
+			c3.68-3.04,5.792-7.584,5.792-12.32v-448C288,27.222,285.888,22.71,282.208,19.67z M256,460.694L32,418.71V93.27l224-41.984
+			V460.694z" fill="#3498db"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M496,79.99H272c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h208v288H272c-8.832,0-16,7.168-16,16
+			c0,8.832,7.168,16,16,16h224c8.832,0,16-7.168,16-16v-320C512,87.158,504.832,79.99,496,79.99z" fill="#3498db"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,143.99H272c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h160c8.832,0,16-7.168,16-16
+			C448,151.158,440.832,143.99,432,143.99z" fill="#3498db"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,207.99H272c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h160c8.832,0,16-7.168,16-16
+			C448,215.158,440.832,207.99,432,207.99z" fill="#3498db"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,271.99H272c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h160c8.832,0,16-7.168,16-16
+			C448,279.158,440.832,271.99,432,271.99z" fill="#3498db"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M432,335.99H272c-8.832,0-16,7.168-16,16c0,8.832,7.168,16,16,16h160c8.832,0,16-7.168,16-16
+			C448,343.158,440.832,335.99,432,335.99z" fill="#3498db"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M209.76,176.086c-8.48-0.864-16.704,5.344-17.664,14.112l-8.608,77.504l-24.512-65.344
+			c-4.704-12.48-25.312-12.48-29.984,0l-26.016,69.408l-7.136-50.048c-1.248-8.768-9.44-14.976-18.112-13.568
+			c-8.736,1.248-14.816,9.344-13.568,18.08l16,112c1.024,7.264,6.848,12.896,14.112,13.664c7.424,0.736,14.144-3.424,16.704-10.272
+			L144,253.558l33.024,88.064c2.368,6.272,8.384,10.368,14.976,10.368c0.672,0,1.312-0.032,1.984-0.16
+			c7.328-0.896,13.088-6.752,13.92-14.08l16-144C224.864,184.982,218.56,177.078,209.76,176.086z" fill="#3498db"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 13 - 0
plugin/document/js/component.js

@@ -0,0 +1,13 @@
+function init_components_library(input){
+	var data =  input.data();
+	var doc = new DocumentApi(input);
+
+	doc.options.panels.tree.visible = data.treePanel;
+	doc.options.panels.search.visible = data.searchPanel;
+	doc.options.panels.detail.visible = data.detailPanel;
+	doc.options.root = data.root;
+	doc.options.rootLabel = data.rootLabel;
+
+	doc.load();
+}
+

+ 1530 - 0
plugin/document/js/document.api.js

@@ -0,0 +1,1530 @@
+
+/*
+# Documentation API
+
+## Instanciation
+
+	ex:
+	``
+	var document = new DocumentApi('#ma-div',options);
+	document.load();
+	``
+
+## Informations 
+
+	document.currentFolder : dossier visualisé
+	document.currentView : vue en cours
+	document.selected : item sélectionné
+
+## Panels
+
+	ex:
+	``document.showPanel('tree');``
+	``document.hidePanel('detail');``
+
+	* tree : arborescence de gauche
+	* files : liste des fichiers
+	* search : moteur de recherche
+	* breadcrumb : fil d'arianne
+	* detail : panel de détail fichier de droite
+
+
+
+## Events
+
+	ex: 
+	``document.on('moved',function(data){ ...  });``
+
+	* loaded : html de l'interface chargé
+	* searched : recherche de fichiers {request, response}
+	* renamed : renommage d'un élement {path ,old_label, label}
+	* moved : déplacement d'un élements {from, to, response}
+	* uploaded : upload d'un ou plusieurs fichiers {data};
+	* folder-created : dossier créé {path, folder, response}
+	* selected : sélection d'un élement {element, path, type, id}
+	* deleted : suppression d'un élement {element, response, path}
+	* executed : execution d'un fichier {element, path};
+	* opened : ouverture d'un dossier {element, path};
+	* downloaded : téléchargement d'un élement {element, path};
+	* properties-showed : affichage des propriétés d'un élement {modal, id ;
+	* right-saved : ajout d'un droit {uid, read, edit, recursive};
+	* right-deleted : suppression d'un droit {id,element};
+	* view-loaded : chargement d'une vue {view:view}
+
+## Ajouter des bouttons au panel de détail
+
+	### ajout d'un boutton classique : 
+		ex:
+		``
+		var document = new DocumentApi('#ma-div',options);
+		document.options.panels.detail.buttons.push({
+			label : 'Custom button',
+			buttonClass : 'btn-custom',
+			lineClass : 'li-class-custom',
+			icon : 'fas fa-times',
+		});
+		document.load();
+		``
+
+	### ajout d'un boutton pour les dossiers uniquement : 
+
+		ex:
+		``
+		var document = new DocumentApi('#ma-div',options);
+		document.options.panels.detail.buttons.push({
+			label : 'Custom button',
+			buttonClass : 'btn-custom',
+			lineClass : 'li-class-custom',
+			icon : 'fas fa-times',
+			visibility : ['directory']
+		});
+		document.load();
+		``
+
+		### custom html : 
+		``
+		var document = new DocumentApi('#ma-div',options);
+		document.options.panels.detail.buttons.push({
+			html : '<div>Mon htl custom ici</div>',
+		});
+		document.load();
+	``
+
+## Ajouter des boutons à la creation d'elements
+		var document = new DocumentApi('#ma-div',options);
+		document.options.panels.search.buttons.push({
+				lineClass : 'create-txt-button',
+				buttonClass : '',
+				icon : 'fas fa-pen text-secondary',
+				label : 'Créer un fichier CUSTOM'					
+		});
+		document.load();
+
+## Vues
+### Changer la vue des fichiers
+
+	``
+	var document = new DocumentApi('#ma-div',options);
+
+	document.view('list');
+
+	document.load();
+	
+	``
+
+	ou après chargement
+
+	``
+	var document = new DocumentApi('#ma-div',options);
+	document.load();
+	document.view('list');
+
+	``
+
+	Les vues par défaut sont : 
+	* list : affichage en liste classique
+	* grid : affichage en grille
+
+
+### Créer une vue custom
+	
+	``
+	var doc = new DocumentApi('#document-container',urlOptions);
+	html = '<ul><li data-path="{{path}}" data-type="{{type}}" data-id="{{id}}" class="file-element hidden p-3"><img style="width:20px"  class="element-thumbnail" data-src="{{thumbnail}}"/>  {{label}} {{sizeReadable}} {{creator}}</li></ul>';
+	doc.addView({
+			uid : 'custom',
+			label : 'Vue personnalisée',
+			icon : 'fas fa-th',
+			html : html
+	});
+	
+	doc.view('custom');
+	doc.load();
+	``
+
+	nb : Le html de la vue doit contenir un élement racine contenant un élement enfant qui servira de model pour l'affichage fichier, cet element
+	doit contenir les attributs et classes comme dans l'example suivant : 
+	``
+	<ul>
+		<li data-path="{{path}}" data-type="{{type}}" data-id="{{id}}" class="file-element hidden"> hello {{label}} !<li>
+	</ul>
+	``
+
+	Une ligne fichier peut contenir les tags suivants :
+	* childNumber: nombre d'enfants (pour les dossiers uniquement)
+	* created: timestamp de création ex : 1564997272
+	* creator: login du créateur ex :  "admin"
+	* extension: extention si fichier ex : "jpg"
+	* id: id en base de données ex : 68
+	* label: libellé du fichier ex : "09.jpg"
+	* path: chemin relative du fichier à partir de la racine des documents ex : "./09.jpg"
+	* size: taille en octet ex :  533172
+	* sizeReadable: taille au format human readable ex : "520.68 ko"
+	* thumbnail: chemin vers l'image d'apercu ex :  "plugin/document/img/file-types/image.svg"
+	* type: type d'élément (file ou directory) ex : "file"
+	* updatedReadable : Date de derniere modification au format human readable ex : "Vendredi 02 Aout 2019 à 18:02"
+	* updatedRelative: Date de derniere modification au format relatif ex :  Dat "Il y a 2 jours"
+	* updated: timestamp de la derniere modification
+	* updater: login du dernier modifieur
+
+## Récuperation de la vue courante
+	document.currentView (string)
+
+
+## Récuperation de l'élement en cours de sélection
+	document.selected : {element,id,path,type}
+
+## Autres options
+
+	``
+	var document = new DocumentApi('#ma-div',options);
+	document.options.rootLabel = 'D:';
+	document.load();
+	``
+
+	* rootLabel : Libellé de la racine
+	* rightEdit : Activer l'edition
+	* rightDelete : Activer la suppression des élements
+	* rightPermission : Activer la gestion des permissions
+
+*/
+
+class DocumentApi {
+	constructor(element,options) {
+			
+			this.onEvents = {};
+			this.currentView = 'list';
+			this.currentFolder = '.';
+			this.views = [];
+			this.selected = null;
+			this.isProcessing = false;
+			this.dom = {
+				container :  $(element),
+				panels : {
+					tree : null,
+					detail : null,
+					search : null,
+					breadcrumb : null,
+					files : null
+				}
+			}
+
+			this.options = {
+				rootLabel : 'C:',
+				root : '',
+				rightEdit : true,
+				rightDelete : true,
+				rightPermission : true,
+				panels : {
+					detail : { 
+						visible : true,
+						buttons : []
+					},
+					view : { 
+						visible : true,
+						buttons : []
+					},
+					tree : { 
+						visible : true
+					},
+					search : { 
+						visible : true,
+						buttons : []
+					},
+					breadcrumb : { 
+						visible : true
+					}
+				},
+				actions : {
+					template : 'document_load_template',
+					element_search : 'document_element_search',
+					folder_create : 'document_folder_create',
+					element_rename : 'document_element_rename',
+					element_delete : 'document_element_delete',
+					element_move : 'document_element_move',
+					element_share_edit : 'document_element_share_edit',
+					element_preview : 'document_element_preview',
+					element_execute : 'document_element_execute',
+					element_upload : 'document_element_upload',
+					element_edit : 'document_element_edit',
+					element_save : 'document_element_save',
+					properties_show : 'document_properties_show',
+					right_search : 'document_right_search',
+					right_save : 'document_right_save',
+					right_delete : 'document_right_delete',
+				}
+			};
+	
+			
+			if(options) this.options = $.extend(this.options,options);
+
+	}
+	load(){
+
+			this.dom.container.html('Chargement...');
+			var element = this.element;
+			var object = this;
+
+			
+			object.options.panels.detail.buttons.push({
+								buttonClass : 'btn-primary btn-download',
+								icon : 'fas fa-arrow-alt-circle-down',
+								label : 'TÉLÉCHARGER',
+								visibility : ['file','directory']
+			});
+
+			object.options.panels.detail.buttons.push({
+								lineClass : 'properties-button directory-button',
+								buttonClass : 'btn',
+								icon : 'fas fa-align-justify',
+								label : 'PROPRIÉTÉS',
+								visibility : ['file','directory']
+			});
+
+
+
+
+			object.options.panels.search.buttons.push({
+								lineClass : 'upload-button',
+								buttonClass : '',
+								icon : 'far fa-file-alt',
+								label : 'Envoyer un fichier',
+								afterHtml : '<form class="box" method="post" action="action.php?action='+object.options.actions.element_upload+'" enctype="multipart/form-data"><input type="file" name="file[]"  multiple /></form>'
+								
+			});
+			object.options.panels.search.buttons.push({
+								lineClass : '',
+								buttonClass : 'add-new-folder',
+								icon : 'far fa-folder',
+								label : 'Créer un dossier',
+								afterHtml : '<div  class="new-folder-block hidden"> \
+												<div class="input-group mb-3">\
+													<input type="text" class="form-control folder-name" placeholder="Nom du dossier">\
+													<div class="input-group-append">\
+														<span class="btn btn-success"><i class="fas fa-check"></i></span>\
+													</div>\
+												</div>\
+											</div>'
+								
+			});
+
+			object.options.panels.search.buttons.push({
+								lineClass : 'create-txt-button',
+								buttonClass : '',
+								icon : 'fas fa-pen text-secondary',
+								label : 'Créer un fichier'
+								
+								
+			});
+		
+
+			if(object.options.rightPermission){
+
+					object.options.panels.detail.buttons.push({
+								lineClass : 'share-button directory-button',
+								buttonClass : 'btn',
+								icon : 'fas fa-share',
+								label : 'PERMISSIONS',
+								visibility : ['file','directory']
+					});
+					object.options.panels.detail.buttons.push({
+							
+								html : '<div class="right-panel hidden"><hr/>\
+										<h6>PARTAGES</h6>\
+										<div class="element-right-form">\
+											<input class="uid" class="form-control" type="text" data-type="user" data-types="user,rank" placeholder="Rang ou utilisateur..." />\
+											<label><input class="read" type="checkbox" data-type="checkbox"> Lecture</label>\
+											<label><input class="edit" type="checkbox" data-type="checkbox"> Écriture</label>\
+											<label><input class="recursive" type="checkbox" data-type="checkbox"> Récursif</label>\
+											<div class="btn btn-add-share btn-light"><i class="fa fa-plus"></i> Ajouter</div>\
+										</div>\
+										<ul class="table element-rights" >\
+											<li data-id="{{id}}" class="hidden p-2">\
+												{{{uid}}} <br>\
+												<small class="text-muted p-0">\
+													<span class="p-2">{{#read}}<i class="fa fa-check text-success"></i>{{/read}}{{^read}}<i class="fa fa-times text-secondary"></i>{{/read}} Lecture </span>\
+													<span class="p-2">{{#edit}}<i class="fa fa-check text-success"></i>{{/edit}}{{^edit}}<i class="fa fa-times text-secondary"></i>{{/edit}} Écriture </span>\
+													<span class="p-2">{{#recursive}}<i class="fa fa-check text-success"></i>{{/recursive}}{{^recursive}}<i class="fa fa-times text-secondary"></i>{{/recursive}} Récursif </span>\
+													<i title="Supprimer la permission" class="fa fa-trash pointer right btn-delete-right" ></i>\
+												</small>\
+											</li>\
+										</ul>\
+										<hr/></div>',
+								visibility : ['file','directory']
+					});
+			}
+
+			if(object.options.rightDelete){
+				object.options.panels.detail.buttons.push({
+					buttonClass : 'btn text-danger btn-element-delete',
+					icon : 'far fa-trash-alt',
+					label : 'SUPPRIMER',
+					visibility : ['file','directory']
+				});			
+			}
+
+			object.dom.container.load('action.php?action='+object.options.actions.template,function(){
+				object.dom.panels =  {
+					tree : $('.tree-panel',object.dom.container),
+					detail : $('.detail-panel',object.dom.container),
+					search : $('.search-module',object.dom.container),
+					breadcrumb : $('.breadcrumb-module',object.dom.container),
+					files : $('.file-module',object.dom.container)
+				};
+
+				if(object.options.root) $('.document-container').attr('data-root',object.options.root);
+
+				if(object.options.panels['tree'].visible === false) object.hidePanel('tree');
+				if(object.options.panels['detail'].visible  === false) object.hidePanel('detail');
+				if(object.options.panels['search'].visible  === false) object.hidePanel('search');
+				if(object.options.panels['breadcrumb'].visible  === false) object.hidePanel('breadcrumb');
+
+				
+				$('.root-folder span',object.dom.panels.tree).text(object.options.rootLabel);
+				
+				object.views.push({
+						uid : 'list',
+						icon : 'fas fa-align-justify',
+						label : 'Vue liste',
+						html : $('.file-view[data-view="list"]').get(0).outerHTML
+				});
+
+				object.views.push({
+						uid : 'grid',
+						icon : 'fas fa-th-large',
+						label : 'Vue grille',
+						html : $('.file-view[data-view="grid"]').get(0).outerHTML
+				});
+				
+
+				//Chargement des vues disponibles
+				object.loadViews();
+				
+				//Rendu de la vue courante
+				object.renderView();
+
+				//Chargement des fichiers
+				object.element_search();
+
+
+				//Chargement des boutons ajouter (dossier, fichier..)
+				$('.document-create-dropdown .dropdown-item:not(.hidden)').remove();
+				var tpl = $('.document-create-dropdown .dropdown-item.hidden').get(0).outerHTML;
+				for(var k in object.options.panels.search.buttons){
+					var data = object.options.panels.search.buttons[k];
+					var button = $(Mustache.render(tpl,data));
+					button.removeClass('hidden');
+					$('.document-create-dropdown').append(button);
+				}
+
+				
+
+
+				$('.root-folder',object.dom.container).addClass('folder-focused');
+
+				$(document).off('keyup').on('keyup', function(e){
+					switch(e.keyCode){
+						case 46:
+							if(!object.options.rightDelete) break;
+							object.element_delete();
+						break;
+						case 113:
+							if(!object.options.rightEdit) break;
+						if ($('.file-view tr').hasClass('element-focused'))
+							object.element_rename_edit($('.element-rename',object.selected.element));
+						break;
+						case 13:
+						if($('.label-search',object.dom.panels.search).is(":focus"))
+							object.element_search();
+						break;
+					}
+				});
+				
+
+
+				$('.file-view:visible').sortable_table({
+					onSort : function(){object.element_search();}
+				});
+
+			//RESET KEYWORD
+			$(".label-search").on('keyup',function(){
+				if ($(this).val() != ''){
+					$('.doc-search-container').addClass('typing');
+				}else{
+					$('.doc-search-container').removeClass('typing');
+				}
+			});
+			
+			$(".search-clear").click(function(){
+				$('.doc-search-container').removeClass('typing');
+
+				$(".label-search").val('');
+				$(".label-search").focus();
+				object.element_search();
+			});
+			$('div.file-panel').on('click',function(e){
+				if(e.target == (this)) {
+					$('.file-view:visible tr').removeClass('element-focused');
+					object.reset_preview();
+				}
+			});
+
+
+			// Mapping des events DOM / objet
+			$('.root-folder',object.dom.panels.tree).click(function(event){object.element_search('.');});
+			$('.btn-search',object.dom.panels.search).click(function(){object.element_search();});
+			$('.add-new-folder',object.dom.panels.search).click(function(event){  object.new_folder(event); });
+			$('.new-folder-block .input-group-append',object.dom.panels.search).click(function(){object.folder_create()});
+
+			$('.detail-buttons',object.dom.panels.detail).on('click','.btn-download',function(){object.element_download(null,true)});
+			$('.detail-buttons',object.dom.panels.detail).on('click','.share-button',function(){object.right_toggle()});
+			$('.detail-buttons',object.dom.panels.detail).on('click','.properties-button',function(){object.properties_show()});
+
+			$('.detail-buttons',object.dom.panels.detail).on('click','.element-right-form .btn-add-share',function(){object.right_save()});
+			$('.detail-buttons',object.dom.panels.detail).on('click','.btn-delete-right',function(){object.right_delete(this)});
+			$('.detail-buttons',object.dom.panels.detail).on('click','.btn-element-delete',function(){object.element_delete();});
+
+			$('.tree-folders',object.dom.panels.tree).on('click','li',function(event){object.folder_toggle(this,event);});
+			
+			//editeur
+			$('.create-txt-button',object.dom.panels.search).click(function(){object.element_edit();});
+			$('.btn-editor-cancel',object.dom.panels.file).click(function(){ $('.file-editor',object.dom.panels.file).addClass('hidden'); });
+			$('.btn-editor-save',object.dom.panels.file).click(function(){object.element_edit_save();});
+
+			if(!object.options.rightEdit){
+				$('.document-add-element',object.dom.panels.file).addClass('hidden');
+			}else{
+				object.init_upload();
+			}
+
+			
+			object.triggerEvent('loaded');
+			
+		});
+
+		}
+
+	triggerEvent(event,options){
+		if(!this.onEvents[event]) return;
+		for(var key in this.onEvents[event]){
+			this.onEvents[event][key](options);
+		}
+	}
+	hidePanel(panel){
+		var object = this;
+		object.options.panels[panel].visible = false;
+		if(object.dom.panels[panel]!=null) object.dom.panels[panel].addClass('hidden');
+	}
+	showPanel(panel){
+		var object = this;
+		object.options.panels[panel].visible = true;
+		if(object.dom.panels[panel]!=null) object.dom.panels[panel].removeClass('hidden');
+	}
+
+
+	//Récuperation d'une liste de element dans le tableau elements
+	element_search(folderPath){
+		
+		var object = this;
+		if(object.isProcessing) return;
+
+		$('.file-preloader',object.dom.panels.files).removeClass('hidden');
+
+		var keyword = $('.label-search',object.dom.panels.search).val();
+
+		//tri
+		var sort = {};
+		var sortElement = $('.file-view:visible th[data-sortable][data-sort]',object.dom.panels.files);
+
+		if(sortElement.length!=0 && sortElement.attr('data-sort')!=""){
+			sort.column = sortElement.attr('data-sortable');
+			sort.sort = sortElement.attr('data-sort');
+		}
+		
+		var data = {
+			/*async: false,*/
+			action: object.options.actions.element_search,
+			keyword : keyword,
+			sort : sort,
+			root :object.options.root,
+			folder : folderPath ? folderPath : object.currentFolder
+		}
+		data.folder = data.folder == '.' ? '': data.folder;
+
+		object.isProcessing = true;
+		$('.file-view:visible',object.dom.panels.files).fill(data,function(response){
+
+			object.currentFolder = data.folder;
+			$('.file-preloader',object.dom.panels.files).addClass('hidden');
+
+			object.triggerEvent('searched',{data:data,response:response});
+
+			object.isProcessing = false;
+
+
+			var breadcrumb = null;
+			if(data.folder) breadcrumb = data.folder.split('/');
+
+			//gestion de l'arborescence de gauche (tree)
+			var tree = $('ul.tree-folders',object.dom.panels.tree);
+	  		var tpl = $('li:not(:visible)',tree).get(0).outerHTML;
+	  		var recipient = data.folder=='' ? tree : $('> ul li[data-path="'+data.folder+'"] > ul',object.dom.panels.tree) ;
+	  		
+
+	  		recipient.find('li:visible').remove();
+
+	  		//On n'affiche l'arborcence que si on est pas en mode recherche
+	  		
+	  		if(!data.keyword || data.keyword ==''){
+
+		  		for(var k in response.rows){
+
+		  			var row = response.rows[k];
+		  			if(row.type!='directory') continue;
+
+		  			var li = $(Mustache.render(tpl,row));
+		  			li.removeClass('hidden');
+		  			recipient.append(li);
+		  		}
+
+		  		if(data.folder){
+			  		$('li',object.dom.panels.tree).removeClass('folder-open folder-focused');
+			  		var crumb = '';
+			  		for(var k in breadcrumb){
+			  			if(k!=0) crumb += '/';
+			  			crumb += breadcrumb[k];
+			  			$('li[data-path="'+crumb+'"]',object.dom.panels.tree).addClass('folder-open');
+			  		}
+			  		$('li[data-path="'+data.folder+'"]',object.dom.panels.tree).addClass('folder-focused');
+		  		}
+	  		}
+			//
+
+		
+			//Fil d'arianne
+			object.breadcrumb_render(breadcrumb);
+
+			//reset panel detail
+			object.reset_preview();
+		
+			
+			
+
+			//EVENTS
+
+			//Prévisualisation
+			$('.file-view:visible .file-element',object.dom.panels.files).click(function(event){ object.element_preview(this, event);  });
+			//Ouverture fichier / dossier
+			$('.file-view:visible .file-element',object.dom.panels.files).dblclick(function(event){ object.element_execute(this);  });
+			
+
+
+			
+			//Renommage
+			$('.file-view:visible .file-element .element-rename',object.dom.panels.files).click(function(event){ 
+				event.stopPropagation();
+	 			event.preventDefault();
+				object.element_rename_edit(this);
+			});
+			//Validation du renommage
+			$('.rename-input',object.dom.panels.files).bind('blur keyup', function(e){
+				var input = $(this);
+				var text = input.next('span');
+				var label = text.text();
+				if(e.type === 'keyup'){
+					//adapte la largeur en temps réel de l'input à  la largeur du texte avec un max de 320 px et un min de 45px
+			 		var width = Math.min(Math.max( input.val().length *5.5 ,45),320);
+			 		$(this).width(width+15);
+				}
+
+				if (e.type !== 'blur' && e.keyCode !== 13) return;
+				
+				var tr = input.closest('.file-element');
+				var button = tr.find('.element-rename');
+
+				var newLabel = input.val().replace(/\s+$/, '');
+			
+				//Les dossiers ne peuvent finir par un .
+				if(newLabel.slice(-1) == '.' && tr.attr('data-type') =='directory') newLabel = newLabel.slice(0,-1);
+
+				//Check si le label a changé ou non
+				if(newLabel == label){
+					text.text(label).removeClass('hidden');
+					input.addClass('hidden');
+					button.removeClass('hidden');
+					return;
+				}
+
+				//Attribution nouveau label
+				text.text(newLabel).removeClass('hidden');
+				input.addClass('hidden');
+		 		
+				$.action({
+					action : object.options.actions.element_rename,
+					path : tr.attr('data-path'),
+					label : newLabel
+				}, function(r){
+					
+					button.removeClass('hidden');
+					$('.tree-folders li[data-path="'+ tr.attr('data-path')+'"]').attr('data-path',r.element.path).find('span').text(newLabel);
+					tr.attr('data-path', r.element.path);
+					object.triggerEvent('renamed',{
+						path : tr.attr('data-path'),
+						old_label : label,
+						label : newLabel
+					});
+
+				}, function(r){
+					text.text(label);
+					button.removeClass('hidden');
+				});
+				
+			});
+			
+			//Maj du placeholder de la recherche
+	 		$('.label-search').attr('placeholder','Rechercher'+(breadcrumb ? ' dans '+breadcrumb[breadcrumb.length-1]:''));
+
+
+	 		if(!object.options.rightEdit){
+				$('.element-rename',object.dom.panels.file).addClass('hidden');
+			}else{
+
+		 		//Drag & Drop fichiers
+		 		var viewElement = $(".file-view:visible");
+		 		var droppableContainer = viewElement.get(0).tagName == 'TABLE' ?  ' tbody': '';
+				$(".file-view:visible"+droppableContainer,object.dom.panels.files).sortable({
+					distance: 5,
+					opacity: 0.75,
+					start: function(e, ui) {
+						var placeholder = ui.item.clone().addClass('original-placeholder').removeAttr('style');
+						ui.item.after(placeholder);
+					},
+					stop: function(e, ui) {
+						$('.original-placeholder').remove();
+						$(e.originalEvent.target).one('click', function(e){
+							e.stopImmediatePropagation(); 
+						});
+					}
+				}).disableSelection();
+
+				$(".file-view:visible .file-element",object.dom.panels.files).droppable({
+					tolerance: "pointer",
+					hoverClass: "folder-receive-element",
+					drop: function(event, ui) {
+						var from = $(ui.draggable["0"]);
+						var to = $(this);
+						if(to.attr('data-type') != 'directory') return;
+
+						$.action({
+							action : object.options.actions.element_move,
+							from : from.attr('data-path'),
+							to : to.attr('data-path'),
+						}, function(response){
+							if (response.element !== null && typeof response.element === 'object') {
+								object.triggerEvent('moved',{from:from,to:to,response:response});
+								from.remove();
+								object.remove_treeline(from);
+								object.reset_preview();
+							} else {
+								alert('Action impossible, un élément existe déjà avec ce nom dans le dossier.');
+							}
+						});
+					}
+				});
+
+				//Drag & drop arborescence
+				$("li",object.dom.panels.tree).droppable({
+					tolerance: "pointer",
+					greedy: true,
+					over: function(e, ui){
+						var to = $(this);
+						var toPath = to.hasClass('root-folder') ? '.' : to.attr('data-path');
+						to.addClass( to.hasClass('root-folder') ?'folder-focused':'folder-hover');
+					},
+					out: function(e, ui){
+						var to = $(this);
+						if( to.hasClass('root-folder') && to.hasClass('folder-focused')) 
+							to.removeClass('folder-focused');
+						if(to.hasClass('folder-hover')) 
+							to.removeClass('folder-hover');
+					},
+					drop: function(event, ui) {
+						var from = $(ui.draggable["0"]);
+						var fromPath = from.attr('data-path');
+						var to = $(this);
+
+						var toPath = to.hasClass('root-folder') ? '.' : to.attr('data-path');
+
+						if( to.hasClass('root-folder') && to.hasClass('folder-focused'))
+							to.removeClass('folder-focused');
+						if(to.hasClass('folder-hover'))
+							to.removeClass('folder-hover');
+						$('.tree-panel li').removeClass('folder-hover');
+						if(fromPath == toPath) {
+							$.message('error', 'Impossible de déplacer un dossier dans lui-même.');
+							return;
+						}
+						$.action({
+							action : object.options.actions.element_move,
+							from : fromPath,
+							to : toPath,
+						}, function(response){
+							if (response.element !== null && typeof response.element === 'object') {
+
+								object.triggerEvent('moved',{from:from,to:to,response:response});
+
+								// Suppression de la ligne dans le panneau arborescence
+								object.remove_treeline(from);
+								// Ajout d'une ligne dans le panneau de l'arborescence
+								if(from.attr('data-type') == 'directory') {
+									var fromLabel = $('span', from).text();
+									var tpl = $('.tree-folders li:not(:visible)',object.dom.panels.tree).get(0).outerHTML;
+									var toContainer = toPath == '.' ? $('ul.tree-folders',object.dom.panels.tree) : $('>ul.folders-container', to);
+									toContainer.parent().addClass('folder-open');
+									$('i.far.fa-folder',toContainer.parent()).removeClass('fa-folder').addClass('fa-folder-open');
+									var li = $(Mustache.render(tpl,{label:fromLabel,path:toPath+'/'+fromLabel}));
+									li.removeClass('hidden');
+									toContainer.append(li);
+								}
+								// Suppression de la ligne dans le panneau d'élément
+								from.remove();
+								// Reset de la preview
+								object.reset_preview();
+							}
+							else
+								alert('Action impossible, un élément existe déjà avec ce nom dans le dossier.');
+						});
+					}
+				});
+
+
+			}
+
+
+
+		});
+	}
+
+
+	//Draw du fil d'Ariane
+	breadcrumb_render(breadcrumb){
+
+		var object = this;
+		if(!object.dom.panels.breadcrumb) return;
+
+		if(breadcrumb === null) breadcrumb = ['.'];
+		
+		if(breadcrumb[0]!='.') breadcrumb.unshift('.');
+		
+		$('ul li:visible',object.dom.panels.breadcrumb).remove();
+
+		var tpl = $('li:not(:visible)',object.dom.panels.breadcrumb).get(0).outerHTML;
+
+		for(var key in breadcrumb){
+			var label = breadcrumb[key];
+			if(label == '.') label = '<i class="fas fa-home"></i>';
+			var currentpath = breadcrumb.slice(0,key);
+			currentpath.push(breadcrumb[key]);
+			currentpath = currentpath.join('/');
+			var li = $(Mustache.render(tpl,{label:label,path:currentpath}));
+			if(key == breadcrumb.length-1) li.attr('title', 'Dossier courant');
+			$('ul',object.dom.panels.breadcrumb).append(li);
+			li.removeClass('hidden');
+		}
+		$('ul > li',object.dom.panels.breadcrumb).click(function(){
+			object.element_search( $(this).attr('data-path'));
+		});
+	}
+
+	
+
+	/** DOSSIER */
+	folder_toggle(element, event, force){
+		var object = this;
+		$('.label-search').val('').blur();
+		var li = $(element).closest('li');
+		
+		if (li.hasClass('folder-focused')){
+			var parent = li.parent().parent();
+			object.element_search(parent.attr('data-path'));
+		}else{
+			object.element_search(li.attr('data-path'));
+		}
+		if(event) event.stopPropagation();
+	}
+
+
+
+
+	// Reset du panneau de preview de la GED
+	reset_preview(){
+		var object = this;
+		$('.detail-thumbnail',object.dom.panels.detail).removeAttr('style');
+		$('> h1',object.dom.panels.detail).text('Aucun fichier sélectionné');
+		$('> small > span',object.dom.panels.detail).text('-');
+		$('.detail-buttons',object.dom.panels.detail).addClass('hidden');
+	}
+
+
+	
+
+	// Preloader affiché durant l'upload
+	toggle_preloader_upload(){
+		var object = this;
+		var preloader = $('.preloader-upload-container');
+		
+		if(!preloader.is(':visible')) {
+			preloader.removeClass('hidden');
+			preloader.find('.preloader-upload-close').unbind('click').click(function(){
+				object.toggle_preloader_upload();
+			});
+			$('body').css('overflow', 'hidden');
+		} else {
+			$('body').css('overflow', '');
+			preloader.addClass('hidden');
+		}
+	}
+
+
+	// Init de l'upload
+	init_upload(){
+		var object = this;
+		var form = $('.upload-button form',object.dom.container);
+		var input = form.find('input[type="file"]');
+		var zone = $('.file-panel');
+		var droppedFiles = null;
+		var noBlink = null;
+		
+		var fileQueue = [];
+
+
+		//Upload des images dans le presse papier
+		window.addEventListener("paste", function(event){
+			var clipboardData = event.clipboardData || window.clipboardData;
+			
+		    if(clipboardData.getData('Text').length) return;
+
+			if(event.clipboardData == false || event.clipboardData.items == undefined) return;
+		    var items = event.clipboardData.items;
+
+		    if(items.length == 0) return;
+
+		    fileQueue = [];
+			for (var i = 0; i < items.length; i++) {
+				var f = items[i];
+				if (f.type.indexOf("image") == -1) return;
+				var file = items[i].getAsFile();
+
+				fileQueue.push({
+					method : 'paste',
+					file : file
+				});
+			}
+			form.trigger('submit');
+		   
+		}, false);
+
+
+
+
+		input.addClass('hidden');
+		$('.upload-button > div',object.dom.container).click(function(e){
+			input.trigger('click');
+			e.preventDefault();
+			e.stopPropagation();
+		});
+		input.on('change', function (e) {
+			fileQueue = [];
+			for(var k in this.files){
+				if(this.files[k].name ==null) break;
+				fileQueue.push({
+					method : 'browse',
+					file : this.files[k]
+				});
+			}
+			form.trigger('submit');
+		});
+
+		zone.on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
+			e.preventDefault();
+			e.stopPropagation();
+		})
+		.on('dragover dragenter', function (e) {
+			clearTimeout(noBlink);
+			$('.drag-overlay').css('display', 'block');
+			zone.addClass('drag-over');
+			e.preventDefault();
+			e.stopPropagation();
+		})
+		.on('dragleave dragend drop', function (e) {
+			noBlink = setTimeout(function(){
+				$('.drag-overlay').css('display', 'none');
+				zone.removeClass('drag-over');
+			},500);
+
+			e.preventDefault();
+			e.stopPropagation();
+		})
+		.on('drop', function (e) {
+		    fileQueue = [];
+			if(!e.originalEvent.dataTransfer) return;
+			droppedFiles = e.originalEvent.dataTransfer.files;
+		
+			for (var i=0, f; f=droppedFiles[i]; ++i) {
+
+				if(f.name.indexOf(".")==-1 && f.size%4096 == 0) {
+					$.message('error', 'Impossible d\'envoyer un dossier.');
+					return;
+				}
+				fileQueue.push({
+					method : 'drop',
+					file : droppedFiles[i]
+				});
+			}
+
+			
+			form.trigger('submit');
+			
+		});
+
+		form.on('submit', function (e) {
+			$('.drag-overlay').css('display', 'none');
+			
+			e.preventDefault();  
+			var tpl = $('.upload-files li.hidden').get(0).outerHTML;
+			$('.upload-files li:not(.hidden)').remove();
+		
+			var fileProcessed = [];
+
+			object.toggle_preloader_upload();
+			for( var k in fileQueue){
+				var file = fileQueue[k].file;
+				
+				var line = $(Mustache.render(tpl,{
+					label : file.name,
+					sort : k,
+					size : readable_size(file.size)
+				}));
+				line.removeClass('hidden');
+				$('.upload-files').append(line);
+
+				
+				var ajaxData = new FormData();
+			   	
+
+				ajaxData.append(input.attr('name'), file);
+				ajaxData.append('method', fileQueue[k].method);
+				ajaxData.append('path',  object.currentFolder =='.' ? '' : object.currentFolder);
+				ajaxData.append('sort', k);
+				
+				
+
+				$.ajax({
+					url: form.attr('action'),
+					type: form.attr('method'),
+					data: ajaxData,
+					dataType: 'json',
+					cache: false,
+					contentType: false,
+					processData: false,
+					complete: function (data) {},
+					success: function (response) {
+
+
+						if(!response.error){
+							$('.upload-files li[data-sort="'+response.sort+'"] .upload-file-state').addClass('success').html('<i class="far fa-check-circle"></i> Succès');
+						}else{
+							$('.upload-files li[data-sort="'+response.sort+'"] .upload-file-state').addClass('error').html('<i class="fas fa-exclamation-circle"></i> Erreur :'+response.error);
+						}
+						
+						fileProcessed.push(response);
+
+						$('.preloader-upload-container .upload-state span').text(fileProcessed.length+'/'+fileQueue.length+' fichiers envoyés');
+
+						if(fileQueue.length==fileProcessed.length){
+							object.triggerEvent('uploaded',{data:fileProcessed});
+							$('.document-create-dropdown').removeClass('show');
+							object.element_search();
+							//On cache automatiquement le gestionnaire d'upload si aucune erreure.
+							if($('.upload-file-state.error').length == 0){
+								setTimeout(function(){
+									var preloader = $('.preloader-upload-container');
+									$('body').css('overflow', '');
+									preloader.addClass('hidden');
+								},1500);
+							}
+						}
+					},
+					error: function (data) {
+						$('.document-create-dropdown').removeClass('show');
+						$.message('error',data);
+					},
+					xhr: function() {
+						var xhr = new window.XMLHttpRequest();
+						xhr.sort = k;
+						
+						xhr.upload.addEventListener("progress", function(evt){
+							
+							if (evt.lengthComputable) {
+							var percentComplete = (evt.loaded / evt.total) * 100;
+							percentComplete = Math.round(percentComplete * 100) / 100;
+
+							var progressBar = $('.upload-files li[data-sort="'+xhr.sort+'"] .progress-bar')
+								.css('width',percentComplete+'%')
+								.text(percentComplete+'%')
+								.attr('aria-valuenow',percentComplete);
+					
+					
+							}
+						}, false);
+						return xhr;
+					}
+				});
+
+
+
+			}
+
+			 
+			
+
+
+
+
+		});
+	}
+
+
+	 folder_create(){
+	 	var object = this;
+	 	var path = object.currentFolder =='.' ? '' : object.currentFolder+'/';
+	 	path += $('.folder-name').val();
+	 	var data = {
+	 		action : object.options.actions.folder_create,
+	 		path : path,
+	 		folder : $('.folder-name',object.dom.panels.search).val()
+	 	}
+	 	$.action(data,function(response){
+	 		object.triggerEvent('folder-created',{path:data.path,folder:data.folder,response:response});
+	 		$('.folder-name',object.dom.panels.search).val('').focus();
+	 		$('.new-folder-block',object.dom.panels.search).toggleClass('hidden');
+	 		$('.dropdown-menu',object.dom.panels.search).trigger('click');
+	 		object.element_search();
+	 	});
+	 }
+
+	 new_folder(event){
+	 	var object = this;
+	 	$('.new-folder-block',object.dom.panels.search).toggleClass('hidden');
+	 	$('.new-folder-block input',object.dom.panels.search).focus().enter(function(){object.folder_create()});
+	 	event.stopPropagation();
+	 }
+
+
+	 /** ELEMENT **/
+	 element_rename_edit(button){
+	 	var object = this;
+	 	var button = $(button);
+	 	var span = button.prev('span');
+	 	var text = span.find('span');
+	 	var tr = span.closest('.file-element');
+	 	var label = span.find('span').text();
+	 	var input = $('.rename-input',tr);
+
+	 	//adapte la largeur de l'input à  la largeur du texte avec un max de 320 px et un min de 45px
+	 	var width = Math.min(Math.max(text.width(),45),320);
+	 	
+		button.addClass('hidden');
+		text.addClass('hidden');
+
+		input.removeClass('hidden')
+			.focus()
+			.val(label)
+			.width(width+15)
+			.click(function(event){
+				event.stopPropagation();
+			});
+
+	 }
+
+
+
+	// Met à jour l'arborescence (supprime la line dans l'arborescence sur move ou rename)
+	remove_treeline(from){
+		var parent = $('.folder-focused ul');
+		if(parent.length == 0) parent = $('.tree-panel > ul');
+		$('li',parent).each(function(i,li){ 
+			var li = $(li);
+			if(li.attr('data-path') == from.attr('data-path')) {
+				li.remove();
+			}
+		});
+	}
+
+
+
+	//Récuperation d'une liste de element dans le tableau elements
+	element_preview(element, event){
+		var object = this;
+		var line = $(element);
+
+		if(line.hasClass('element-focused')) return;
+
+		object.selected = {
+			element : line,
+			path : line.attr('data-path'),
+			type : line.attr('data-type'),
+			id : line.attr('data-id')
+		}
+
+		$('.right-panel',object.dom.panels.detail).addClass('hidden');
+
+		$('.detail-buttons',object.dom.panels.detail).removeClass('hidden');
+		if(object.selected.type =='directory') {
+			$('.detail-buttons > .directory-button',object.dom.panels.detail).removeClass('hidden');
+		}else{
+			$('.detail-buttons > .directory-button',object.dom.panels.detail).addClass('hidden');
+		}  
+
+
+
+		$('.detail-buttons > li:visible',object.dom.panels.detail).remove();
+		var buttonTemplate = $('.detail-buttons > li:eq(0).hidden').get(0).outerHTML;
+		if(object.options.panels.detail.buttons){
+			for(var i in object.options.panels.detail.buttons){
+				var data = object.options.panels.detail.buttons[i];
+				if(data.visibility &&  data.visibility.indexOf(object.selected.type) == -1 ) continue;
+				
+				var button = $(Mustache.render(buttonTemplate,data));
+				button.removeClass('hidden');
+				$('.detail-buttons').append(button);
+			}
+		}
+
+		//autoblur sur le rename d'un fichier en cours si existant
+		var currentRename = $('.file-view:visible .file-element .rename-input:visible');
+		if(currentRename.length>0) currentRename.trigger('blur');
+		
+		$('.file-view:visible .file-element',object.dom.panels.files).removeClass('element-focused');
+		line.addClass('element-focused');
+
+		$('.detail-thumbnail .thumbnail-preloader',object.dom.panels.detail).addClass('show');
+
+		$.action({
+			action : object.options.actions.element_preview,
+			path : line.attr('data-path'),
+		},function(response){
+
+			setTimeout(function(){
+				$('.detail-thumbnail .thumbnail-preloader',object.dom.panels.detail).removeClass('show');
+			},100);
+
+			$('.detail-thumbnail',object.dom.panels.detail).css('background-image','url('+response.row.thumbnail+')');
+
+			var substrings = ['.jpg','.jpeg','.bmp','.gif','.png'];
+			$('.detail-thumbnail',object.dom.panels.detail).css('background-size','');
+			if (new RegExp(substrings.join("|")).test(response.row.thumbnail))
+				$('.detail-thumbnail',object.dom.panels.detail).css('background-size','contain');
+
+			$('> h1',object.dom.panels.detail).html(response.row.label).text();
+			var size = response.row.sizeReadable;
+			if(response.row.type == 'directory') size = response.row.childNumber+' élement'+(response.row.childNumber>1?'s':'');
+
+			$('> small > span',object.dom.panels.detail).text(size+", "+response.row.updatedRelative);
+
+			object.triggerEvent('selected',object.selected);
+
+
+		});
+	}
+
+	//Suppression d'un élément de la GED
+	element_delete(){
+
+		var object = this;
+		if($('span .rename-input:visible',object.selected.element).length) return;
+		
+		if(object.selected == null) {
+			$.message('info', 'Sélectionnez d\'abord un élément à supprimer');
+			return;
+		}
+		if(!confirm('Êtes-vous sûr de vouloir supprimer cet élément ?')) return;
+		$.action({
+			action : object.options.actions.element_delete,
+			path : object.selected.path,
+		},function(response){
+
+			object.triggerEvent('deleted',{
+				element : object.selected.element,
+				response : response,
+				path : object.selected.path,
+			});
+
+			object.selected.element.remove();
+			object.reset_preview();
+		});
+	}
+
+	element_share_edit(){
+		var object = this;
+		if($('span .rename-input:visible',object.selected.element).length) return;
+		
+		if(object.selected == null) {
+			$.message('info', 'Sélectionnez d\'abord un élément à partager');
+			return;
+		}
+
+		$.action({
+			action : object.options.actions.element_share_edit,
+			path : object.selected.path,
+		});
+	}
+
+	//edition d'un fichier texte en ligne
+	element_edit(path){
+		var object = this;
+
+		$('.file-editor-name',object.dom.panels.file).val('Nouveau Fichier.txt');
+		$('.file-editor-input',object.dom.panels.file).val('');
+	
+
+		if(path){
+			$('.file-editor-input',object.dom.panels.file).val('Chargement en cours...');
+			$.action({
+				action : object.options.actions.element_edit,
+				path : path,
+			},function(r){
+				$('.file-editor-name',object.dom.panels.file).val(r.label);
+				$('.file-editor-input',object.dom.panels.file).val(r.content);
+			});
+		}
+
+		$('.file-editor',object.dom.panels.file).removeClass('hidden');
+		$('.file-editor-input',object.dom.panels.file).focus();
+	}
+
+	//edition d'un fichier texte en ligne
+	element_edit_save(){
+		var object = this;
+		$.action({
+			action : object.options.actions.element_save,
+			label : $('.file-editor-name',object.dom.panels.file).val(),
+			content : $('.file-editor-input',object.dom.panels.file).val(),
+			path : object.currentFolder,
+		},function(r){
+			$('.file-editor',object.dom.panels.file).addClass('hidden');
+			object.element_search();
+		});
+	}
+
+	//Récuperation d'une liste de element dans le tableau elements
+	element_execute(element){
+		var object = this;
+		var inputSearch = $('.label-search',object.dom.panels.search);
+		if(inputSearch.val().length) {
+			inputSearch.val('').blur();
+			$(".search-clear",object.dom.panels.search).removeAttr('style');
+		}
+
+		var line = $(element);
+
+		if(line.attr('data-type') == 'directory'){
+			object.element_search(line.attr('data-path'));
+			object.triggerEvent('opened',{
+				element : line,
+				path : line.attr('data-path')
+			});
+			return;
+		}
+	
+		object.triggerEvent('executed',{
+				element : line,
+				path : line.attr('data-path')
+		});
+
+		var ext = object.selected.path.split('.').pop();
+		switch(ext){
+			case 'txt':
+				object.element_edit(object.selected.path);
+			break;
+			default:
+				object.element_download(line);
+			break;
+		}
+
+		
+	}
+
+	// Téléchargement d'un élément de la GED
+	element_download(line,forceDownload){
+		var object = this;
+		var line = line ? line : object.selected.element;
+		if(line.length == 0) return $.message('info', 'Sélectionnez d\'abord un élément à télécharger');
+			
+		object.triggerEvent('downloaded',{
+				element : line,
+				path : line.attr('data-path')
+		});
+
+		var url = 'action.php?action='+object.options.actions.element_execute+'&path='+encodeURIComponent(btoa(line.attr('data-path')));
+		if(forceDownload) url+= '&download';
+		window.open(url, '_blank');
+	}
+
+
+	/** PROPERTIES **/
+
+	properties_show(){
+		
+		var object = this;
+		var modal = $('.document-properties-modal');
+		var tpl = $('.modal-body-template',modal).html();
+		modal.modal('show');
+
+		$('.modal-body',modal).html('Chargement...');
+		
+		
+		$.action({
+			action : object.options.actions.properties_show,
+			id : object.selected.id
+		},function(r){
+			$('.modal-body',modal).html(Mustache.render(tpl,r.row));
+			init_components(modal);
+			object.triggerEvent('properties-showed',{
+				modal : modal,
+				id : object.selected.id
+			});
+		});
+	}
+
+	/** ELEMENTRIGHT **/
+		//Récuperation d'une liste de elementright dans le tableau elementrights
+	right_toggle(){
+			var object = this;
+			$('.right-panel',object.dom.panels.detail).toggleClass('hidden');
+			init_components('.element-right-form');
+			this.right_search();
+	}
+	//Récuperation d'une liste de elementright dans le tableau elementrights
+	right_search(callback){
+		var object = this;
+	
+		if(object.selected == null) return;
+
+		$('.element-rights').fill({
+			action:object.options.actions.right_search,
+			id : object.selected.id
+		},function(){
+			if(callback!=null) callback();
+		});
+	}
+
+	//Ajout ou modification d'élément elementright
+	right_save(){
+		var object = this;
+	
+		var data = {
+			uid : $('.element-right-form input.uid').val(),
+			read : $('.element-right-form .read').val(),
+			edit : $('.element-right-form .edit').val(),
+			recursive : $('.element-right-form .recursive').val()
+		}
+		data.action = object.options.actions.right_save;
+		data.element = object.selected.id;
+		$.action(data,function(r){
+			$.message('success','Enregistré');
+
+			$('.uid').val('');
+			$('.read,.edit,.recursive').prop('checked',false);
+			init_components('.element-right-form');
+			object.right_search();
+
+			data.id = object.selected.id;
+			data.element = object.selected.element;
+			object.triggerEvent('right-saved',data);
+
+
+		});
+	}
+
+	//Suppression d'élement elementright
+	right_delete(element){
+		var object = this;
+		if(!confirm('Êtes-vous sûr de vouloir supprimer ce droit ?')) return;
+		var line = $(element).closest('li');
+		$.action({
+			action : object.options.actions.right_delete,
+			id : line.attr('data-id')
+		},function(r){
+			object.triggerEvent('right-deleted',{id:line.attr('data-id'),element:line});
+			line.remove();
+		});
+	}
+	//Ajoute un callback sur un evenement de la ged particulier
+	on(event,callback){
+		if(!this.onEvents[event])this.onEvents[event] = [];
+		this.onEvents[event].push(callback);
+	}
+
+	/* Gestion des vues */
+
+	//Change la vue courante
+	view(view){
+		var object = this;
+		object.currentView = view;
+		object.renderView();
+		object.triggerEvent('view-loaded',{view:view});
+	}
+
+	//Ajoute une vue custom
+	addView(data){
+		var object = this;
+		data.html = $(data.html);
+		
+		data.html.attr('data-view',data.uid);
+		if(!data.html.hasClass('file-view')) data.html.addClass('file-view');
+		
+		object.views.push(data);
+	}
+
+	//affiche le rendu de la vue courante (object.currentView)
+	renderView(){
+		var object = this;
+		if(!object.dom.panels.files) return;
+		$('.file-view',object.dom.panels.files).addClass('hidden');
+		var viewElement = $('.file-view[data-view="'+object.currentView+'"]');
+
+		$(object.dom.panels.files).attr('data-view',object.currentView);
+		$('.view-module > li',object.dom.panels.search).removeClass('selected');
+		$('.view-module > li[data-view="'+object.currentView+'"]',object.dom.panels.search).addClass('selected');
+
+		viewElement.removeClass('hidden');
+		object.element_search();
+	}
+
+	//charge toutes les vues disponibles dans le dom
+	loadViews(){
+		var object = this;
+		//Chargement des bouttons de vue
+		$('.view-module > li:visible',object.dom.panels.search).remove();
+		var tpl = $('.view-module > li.hidden',object.dom.panels.search).get(0).outerHTML;
+
+		for(var key in object.views){
+			var view = object.views[key];
+			var button = $(Mustache.render(tpl, view));
+			button.removeClass('hidden');
+			$('.view-module',object.dom.panels.search).append(button);
+			button.click(function(){
+				object.view($(this).attr('data-view'));
+			});
+
+			
+			if($('.file-view[data-view="'+view.uid+'"]',object.dom.panels.files).length==0)
+		 		object.dom.panels.files.append(view.html);
+
+		}
+
+	}
+
+
+}

+ 34 - 0
plugin/document/js/main.js

@@ -0,0 +1,34 @@
+
+
+function init_plugin_document(){
+	switch($.urlParam('page')){
+		default:
+		break;
+	}
+
+	var urlOptions = $.urlParam('data');
+	if(urlOptions && urlOptions!=''){
+		urlOptions = JSON.parse(atob(urlOptions));
+	}
+	
+	var doc = new DocumentApi('#document-container',urlOptions);
+
+	doc.load();
+
+
+
+}
+
+
+/** SETTINGS */
+//Enregistrement des configurations
+function document_setting_save(){
+	$.action({ 
+		action : 'document_setting_save', 
+		fields :  $('#document-setting-form').toJson() 
+	}, function(r){
+		$.message('success','Enregistré');
+	});
+}
+
+

+ 17 - 0
plugin/document/js/widget.js

@@ -0,0 +1,17 @@
+function widget_document_init(){
+	init_components('.widgetDocumentContainer');
+}
+
+
+function document_widget_configure_save(widget,modal){
+
+	var data = $('#document-widget-form').toJson();
+	
+	data.action = 'document_widget_configure_save';
+	data.id = modal.attr('data-widget');
+
+	$.action(data,function(){
+		$.message('success','Configuration enregistrée');
+		dashboard_dashboardwidget_search();
+	});
+}

+ 8 - 0
plugin/document/page.list.php

@@ -0,0 +1,8 @@
+<?php
+global $conf;
+
+User::check_access('document','read');
+require_once(__DIR__.SLASH.'Element.class.php');
+?>
+
+<div id="document-container" data-right-edit="<?php echo $myUser->can('document', 'edit')?'true':'false' ?>" data-right-delete="<?php echo $myUser->can('document', 'delete')?'true':'false' ?>" data-right-permission="<?php echo $myUser->can('document', 'configure')?'true':'false' ?>"></div>

+ 13 - 0
plugin/document/setting.document.php

@@ -0,0 +1,13 @@
+<?php 
+global $myUser,$conf;
+User::check_access('document','configure');
+?>
+<div class="row" id="document-setting">
+	<div class="col-md-12">
+		<br>
+		<div class="btn btn-success float-right" onclick="document_setting_save()"><i class="fas fa-check"></i> Enregistrer</div>
+		<h3>Réglages Documents</h3>
+		<hr/>
+		<?php echo Configuration::html('document'); ?>
+	</div>
+</div>

+ 184 - 0
plugin/document/template.document.php

@@ -0,0 +1,184 @@
+<div class="document-container<?php echo isset($embedded)?' embedded':'' ?>" 
+
+	<?php 
+	if(isset($_['data']) && is_array($_['data'])):
+		foreach($_['data'] as $key=>$value): ?>
+			data-<?php echo $key; ?>="<?php echo $value; ?>" 
+		<?php endforeach;
+	endif; ?>
+	>
+	
+
+	<div class="tree-panel d-none d-md-block">
+		<ul>
+			<li class="pointer root-folder"><i class="fas fa-home"></i> <span></span></li>
+		</ul>
+		<ul class="tree-folders">
+			<li class="hidden folder" title="{{childNumber}} fichiers" data-label="{{label}}" data-path="{{path}}"><i class="far fa-folder"></i> <span>{{label}}</span><ul class="folders-container"></ul></li>
+		</ul>
+	</div>
+
+	<div class="file-panel">
+		<div class="drag-overlay">
+			<div class="overlay-text"><i class="far fa-file"></i>&nbsp;&nbsp;Déposez vos fichiers ici.</div>
+			<div class="overlay-icon"><i class="fas fa-arrow-alt-circle-down"></i></div>
+		</div>
+		<div class="breadcrumb-module">
+			<ul>
+				<li class="hidden" data-path="{{path}}" title="Cliquer pour naviguer dans le dossier">{{{label}}}</li>
+			</ul>
+		</div>
+		<div class="search-module form-inline">
+			<div class="doc-search-container">	
+				<input class="form-control form-control-sm label-search" type="text" placeholder="">
+				<span class="search-clear fas fa-times"></span>
+				<span class="text-muted btn-search" title="Cliquer pour rechercher dans le dossier courant"><i class="fas fa-search"></i></span>
+			</div>
+			
+
+			<div class="dropdown document-add-element">
+				<button class="btn btn-small btn-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-plus"></i> Ajouter</button>
+				<div class="dropdown-menu document-create-dropdown" aria-labelledby="dropdownMenuButton" style="width: 250px;">
+					<a class="dropdown-item {{lineClass}} hidden" href="#">
+						<div class="{{buttonClass}}"><i class='{{icon}}'></i> {{label}}</div>
+						{{{afterHtml}}}
+					</a>
+				</div>
+			</div>
+
+			<ul class="view-module">
+				<li class="hidden"  data-view="{{uid}}"><i title="{{label}}" class="{{icon}} {{class}}"></i></li>
+			</ul>
+
+		</div>
+
+		<div class="file-module">
+			<div class="file-preloader"><i class="fas fa-circle-notch fa-spin"></i></div>
+
+			<div class="file-editor hidden">
+				<div class="file-editor-header"><i class="far fa-file-alt ml-3"></i> <input type="text" class="file-editor-name" value="Nouveau fichier.txt">
+					<div class="btn btn-primary btn-editor-save"><i class="far fa-check-circle"></i> Enregistrer</div>
+					<div class="btn btn-light btn-editor-cancel"><i class="fas fa-ban"></i> Annuler</div>
+					<div class="clear"></div>
+				</div>
+				<textarea class="file-editor-input" placeholder="Aucun contenu pour le moment..."></textarea>
+			</div>
+
+			<!-- Vue liste -->
+			<table class="file-elements-list file-view hidden" data-view="list">
+				<thead>
+					<tr>
+						<th data-sortable="label" class="name-head">Nom</th>
+						<th data-sortable="creator" class="creator-head">Créateur</th>
+						<th data-sortable="size" class="size-head">Taille</th>
+						<th data-sortable="updated" class="updated-head">Modifié le</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr class="hidden file-element" data-path="{{path}}" data-type="{{type}}" data-id="{{id}}" >
+						<td class="name-cell"><img  class="element-thumbnail" data-src="{{icon}}"/> 
+							<span>
+								<input type="text" class="rename-input hidden" value="">
+								<span>{{label}}</span>
+							</span><i title="Renommer" class="fas fa-pencil-alt element-rename"></i>
+						</td>
+						<td class="creator-cell">{{creator}}</td>
+						<td class="size-cell">{{sizeReadable}}</td>
+						<td class="updated-cell" title="{{updatedRelative}}">{{updatedReadable}}</td>
+					</tr>
+				</tbody>
+			</table>
+			<!-- Vue grille -->
+			<ul class="file-elements-grid file-view hidden" data-view="grid">
+				<li class="file-element hidden  element-type-{{extension}}" data-path="{{path}}" data-type="{{type}}" data-id="{{id}}">
+					<div class="grid-container">
+						<div class="element-thumbnail" style="background-image:url({{thumbnail}});"></div>
+						<div  class="element-infos">
+							<div class="name-cell">
+								<span>
+									<input type="text" class="rename-input hidden" value="">
+									<span>{{label}}</span>
+								</span><i title="Renommer" class="fas fa-pencil-alt element-rename"></i>
+							</div>
+							
+							<span class="size-cell">{{#childNumber}}{{childNumber}} éléments{{/childNumber}} {{sizeReadable}}</span> <span class="creator-cell">par {{creator}}</span><br>
+							<span class="updated-cell" title="{{updatedRelative}}"><i class="far fa-calendar"></i> {{updatedReadable}}</span>
+						</div>
+					</div>
+				</li>
+			</ul>
+
+		</div>
+	</div>
+
+	<div class="detail-panel">
+		<div class="detail-thumbnail">
+			<div class="thumbnail-preloader"><i class="fas fa-circle-notch fa-spin"></i></div>
+		</div>
+		<h1 class="text-center">Aucun fichier sélectionné</h1>
+		<small class="informations"><i class="fas fa-star"></i> <span>-</span></small>
+		
+		<ul class="detail-buttons" class="hidden">
+			<li class="hidden {{lineClass}}">{{{html}}}{{^html}}<div class="btn {{buttonClass}}"><i class="{{icon}}"></i> {{label}}</div>{{/html}}</li>
+		</ul>
+	</div>
+
+
+	<!-- upload Preloader -->
+	<div class="preloader-upload-container">
+		<i class="fas fa-times preloader-upload-close"></i>
+		<div class="preloader-upload">
+			<div class="preloader-upload-shadow"></div><div class="preloader-upload-box"></div>
+			<div class="upload-state"><h5>Envois en cours...</h5><span>x/n fichiers</span></div>
+			
+			<ul class="upload-files">
+				<li class="hidden" data-sort="{{sort}}">
+					<div>
+						<h5>{{label}}</h5> <small>{{size}}</small> <span class="upload-file-state"></span>
+					</div>
+					<div class="progress">
+						<div class="progress-bar" role="progressbar" style="width: 20%;" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100">20%</div>
+					</div>
+				</li>
+			</ul>
+		</div>
+	</div>
+</div>
+
+
+
+<!-- Modal Propriété -->
+<div class="modal fade document-properties-modal"  tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
+	<div class="modal-dialog" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<h5 class="modal-title" id="exampleModalLabel">Propriétés</h5>
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+					<span aria-hidden="true">&times;</span>
+				</button>
+			</div>
+			<div class="modal-body">
+
+			</div>
+			<div class="hidden modal-body-template">
+				Libellé : <input class="form-control-plaintext form-control-sm text-info" readonly="readonly" type="text" value="{{label}}">
+				Chemin relatif : <input class="form-control-plaintext form-control-sm text-info" readonly="readonly" type="text" value="{{path}}">
+				Date de création : <input class="form-control-plaintext form-control-sm text-info" readonly="readonly" type="text" value="{{createdLabel}}">
+				Date de modification : <input class="form-control-plaintext form-control-sm text-info" readonly="readonly" type="text" value="{{updatedLabel}}">
+				Propriétaire : <input class="form-control-plaintext form-control-sm text-info" type="text" readonly="readonly" value="{{creator}}">
+				Dernier changement par : <input class="form-control-plaintext form-control-sm text-info" readonly="readonly" type="text" value="{{updater}}">
+				Identifiant du document : <input class="form-control-plaintext form-control-sm text-info" readonly="readonly" type="text" value="#{{id}}">
+				Adresse racine : <input class="form-control-plaintext form-control-sm text-info" type="text" onclick="$(this).select()" readonly="readonly" value="{{rootUrl}}">
+			</div>
+			<div class="modal-footer">
+				<button type="button" class="btn btn-secondary" data-dismiss="modal">Fermer</button>
+			</div>
+		</div>
+	</div>
+
+
+</div>
+
+
+
+

+ 3 - 0
plugin/document/todo.txt

@@ -0,0 +1,3 @@
+- La gestion des droits recursif n'est pour le moment gérée que pour la liste des fichiers (element::browse),Déporter cette feature également sur les autres actions (delete, move, addfile,addfolder,copy)
+- Les entrées en bases ne sont pas supprimées dans le cas ou un fichier est physiquement supprimé depuis l'os
+- Pb d'upload avec les fichiers ayant des simple quotes speciales type ’

+ 23 - 0
plugin/document/widget.configure.php

@@ -0,0 +1,23 @@
+<?php 
+
+User::check_access('document','configure');
+$treepanel = $widget->data('widget-document-tree');
+$treepanel = $treepanel =="" ? 0 : $treepanel ;
+
+$detailpanel = $widget->data('widget-document-detail');
+$detailpanel = $detailpanel =="" ? 0 : $detailpanel ;
+
+$searchpanel = $widget->data('widget-document-search');
+$searchpanel = $searchpanel =="" ? 0 : $searchpanel ;
+
+?>
+<div id="document-widget-form">
+	<label><input type="checkbox" data-type="checkbox" <?php echo $treepanel==1 ? 'checked="checked"' : '' ; ?> id="widget-document-tree"> Afficher le panneau arborescence</label><br>
+	<label><input type="checkbox" data-type="checkbox" <?php echo $detailpanel==1 ? 'checked="checked"' : '' ; ?> id="widget-document-detail"> Afficher le panneau détail</label><br>
+	<label><input type="checkbox" data-type="checkbox" <?php echo $searchpanel==1? 'checked="checked"' : '' ; ?> id="widget-document-search"> Afficher le panneau recherche</label>
+
+	<label>Afficher le chemin relatif suivant :</label>
+	<small class="text-muted">ex : dossier1/sous dossier</small>
+	<input type="text" class="form-control" value="<?php echo $widget->data('widget-document-root'); ?>" id="widget-document-root">
+	
+</div> 

+ 25 - 0
plugin/document/widget.php

@@ -0,0 +1,25 @@
+<?php 
+global $myUser;
+try{
+
+User::check_access('document','read');
+
+?>
+<div class="widgetDocumentContainer">
+	<div data-type="library" 
+	data-root-label="C:"
+	data-root="<?php echo $widget->data('widget-document-root'); ?>"  
+	data-tree-panel="<?php echo $widget->data('widget-document-tree')=='1'? 'true':'false' ?>"  
+	data-search-panel="<?php echo $widget->data('widget-document-search')=='1'? 'true':'false' ?>"  
+	data-detail-panel="<?php echo $widget->data('widget-document-detail')=='1'? 'true':'false' ?>" 
+	></div>
+</div>
+
+<?php
+}catch(Exception $e){ ?>
+ 
+ <div class="alert alert-warning" role="alert">
+  <?php echo $e->getMessage(); ?>
+</div>
+
+<?php } ?>

+ 22 - 0
plugin/hackpoint/action.php

@@ -556,6 +556,28 @@ switch($_['action']){
 	    });
 	    break;
 
+	    case 'hackpoint_resource_git_explore':
+	    	Action::write(function(&$response){
+		        global $myUser,$_;
+	        	
+	        	require_once(__DIR__.SLASH.'..'.SLASH.'document'.SLASH.'Element.class.php');
+	        	require_once(__DIR__.SLASH.'Sketch.class.php');
+	        	require_once(__DIR__.SLASH.'Resource.class.php');
+	        	
+	        	$resource = Resource::getById($_['id']);
+	        	if(!file_exists($resource->directory())) mkdir($resource->directory(),true);
+
+	        	if(!file_exists( Element::root().SLASH.'hackpoint')) symlink (File::dir().'hackpoint' , Element::root().SLASH.'hackpoint' ) ;
+	        	//D:\Workspace\PHP\hackpoint\file\documents\hackpoint\sketch\2\28\*
+	        	system('cd "'.$resource->directory().'" && git clone '.$_['url'].' repo.git');
+	        	$stream ='<div data-type="library" data-root="'.str_replace(array(File::dir(),'\\'),array('','/'),$resource->directory()).'/repo.git"></div>';
+	       
+
+		        
+		        $response['html'] = $stream;
+		    });
+	    break;
+
 	
 }
 ?>

+ 2 - 0
plugin/hackpoint/js/main.js

@@ -618,6 +618,8 @@ function hackpoint_resource_title_edit(event,element){
  	console.log(event,'hey!');
 }
 
+
+
 function hackpoint_resource_mirrorify(element,data){
 
 	if($('.hackpoint').hasClass('readonly')) data.readOnly = true;

+ 85 - 0
plugin/hackpoint/types/GitType.class.php

@@ -0,0 +1,85 @@
+<?php
+require_once(__DIR__.SLASH.'ReadmeType.class.php');
+class GitType extends ReadmeType{
+
+	public static function manifest(){
+		return array(
+			'uid' => 'git',
+			'label' => 'Dépot git',
+			'icon' => 'fab fa-git-square',
+			'color' => '#ffffff',
+			'background' => '#bd2c00',
+			'description' => 'Dépot git associé',
+			'fromExtension' => array('.git'),
+			'toExtension' => '.zip',
+			'default' => '',
+
+		);
+	}
+
+	/* EDITION */
+	public static function toHtml($resource,$sketch=null){
+		$infos = self::manifest();
+		$json = json_decode($resource->content,true);
+			$url = '';
+			$synchronise = false;
+		if($json){
+			$url = $json['url'];
+			$synchronise = $json['synchronise'];
+		}
+		return array(
+			'html'=>'<input id="content" onblur="window[\'hackpoint_resource_git_save\']()" class="git-repository form-control" placeholder="http://domain.com/repository.git" value="'.$url.'"><label><input type="checkbox" data-type="checkbox" onclick="window[\'hackpoint_resource_git_save\']()" '.($synchronise?'checked="checked"':'').' id="git-synchronise"> Synchroniser sur hackpoint</label><div id="git-browser"></div>',
+			'javascript' => "
+				window['hackpoint_resource_git_save'] = function(){
+					if($('.hackpoint').hasClass('readonly')) return;
+					var data = {};
+					data.action = 'hackpoint_resource_save_content';
+					data.id = $('#sketch-editor').attr('data-id');
+					data.content = JSON.stringify({url:$('#content').val(),synchronise:$('#git-synchronise').prop('checked')});
+					$('.sketch-preloader').show();
+					$.action(data,function(r){
+						setTimeout(function(){
+							$('.sketch-preloader').fadeOut(200);
+						},300);
+
+						hackpoint_resource_git_explore();
+
+					});
+				}
+
+				function hackpoint_resource_git_explore(){
+					console.log('ee');
+					var data = {};
+					data.action = 'hackpoint_resource_git_explore';
+					data.url = $('#content').val();
+					data.id = $('#sketch-editor').attr('data-id');
+					$('.sketch-preloader').show();
+					$.action(data,function(r){
+						setTimeout(function(){
+							$('.sketch-preloader').fadeOut(200);
+						},300);
+
+						$('#git-browser').html(r.html);
+						init_components();
+
+					});
+				}
+				$(document).ready(hackpoint_resource_git_explore());
+
+			"
+		);
+	}
+
+	//Export vers un fichier brut
+	public static function toFile($resource){
+		$infos = self::manifest();
+		return array(
+			'name'=> slugify($resource->label).'.'.$infos['toExtension'],
+			'content' => html_entity_decode($resource->content)
+		);
+	}
+
+
+	
+}
+?>

+ 62 - 2
plugin/navigation/MenuItem.class.php

@@ -6,7 +6,7 @@
  * @license copyright
  */
 class MenuItem extends Entity{
-	public $id,$icon,$label,$url,$classes,$sort,$parent,$user,$target,$editable,$slug,$childs;
+	public $id,$icon,$label,$url,$classes,$sort,$parent,$user,$target,$editable,$slug,$childs,$visibility,$menu;
 	public $TABLE_NAME = 'menuitem';
 	public $fields =
 	array(
@@ -15,18 +15,78 @@ class MenuItem extends Entity{
 		'label' => 'string',
 		'slug' => 'string',
 		'url' => 'longstring',
+		'visibility' => 'longstring',
 		'classes' => 'string',
 		'sort' => 'int',
 		'user' => 'string',
 		'target' => 'string',
 		'editable' => 'int',
-		'parent' => 'int'
+		'parent' => 'int',
+		'menu' => 'int'
 	);
 
+	public $indexes = array('slug','menu');
+
 	function __construct(){
 		parent::__construct();
 		$this->editable = true;
+		$this->menu = 0;
+		$this->user = '';
+	}
+
+	public static function target($slug=null){
+		$targets = array(
+			'redirect' => array('label'=>'Redirection','slug'=>'redirect','icon'=>'fas fa-link'),
+			'_blank' => array('label'=>'Redirection  (nouvel onglet)','slug'=>'_blank','icon'=>'fas fa-link'),
+			'iframe' => array('label'=>'IFrame','slug'=>'iframe','icon'=>'fas fa-crop'),
+		);
+
+		$undefined = array('label'=>'Non définis','slug'=>'','icon'=>'');
+
+		if(!isset($slug)) return $targets;
+
+		return isset($targets[$slug]) ? $targets[$slug] : $undefined;
+	}
+
+	public function visibleFor($user){
+		if(!isset($this->visibility) || $this->visibility=='' || $user->superadmin) return true;
+		$visibility =  explode(',', $this->visibility);
+		
+		foreach($visibility as $entity){
+			if(is_numeric($entity)){
+				if($user->hasRank($entity)) return true;
+			}else{
+				if($user->login==$entity) return true;
+			}
+		}
+		return false;
+		
 	}
 
+	public static function bySlug($menuSlug,$user=''){
+		$menuItems = array();
+
+		$mainMenu = array();
+		foreach(self::staticQuery('SELECT it.* FROM {{table}} it LEFT JOIN {{table}} me ON it.menu=me.id WHERE me.slug=? and me.user=? ORDER BY sort',array($menuSlug,$user),true) as $dbItem){
+			$mainMenu[] = $dbItem->toArray(); 
+		}
+
+		uasort ($mainMenu , function($a,$b){return $a['sort']>$b['sort']?1:-1;});
+		foreach ($mainMenu as $i=>$item) {
+			if(isset($item['parent'])) continue;
+			$menuItems[$item['id']] = $item;
+			$menuItems[$item['id']]['childs'] = array();
+			unset($mainMenu[$i]);
+		}
+
+		foreach ($mainMenu as $item) {
+			$menuItems[$item['parent']]['childs'][] = $item;
+		}
+
+		
+		return $menuItems;
+	}
+
+
 }
 ?>

+ 177 - 85
plugin/navigation/action.php

@@ -1,121 +1,213 @@
 <?php
 global $_,$conf;
 switch($_['action']){
-	/** MENUITEM **/
 	
+	//permet le log/surcouhe d'une redirection de navigation
+	case 'navigation_redirect':
+		$url = base64_decode($_['url']);
+		Log::put("Ouverture de l'url: ".$url,'navigation');
+		Plugin::callHook('navigation_redirect',array($url));
+		header('location: '.$url);
+	break;
+
+
 	//Récuperation d'une liste de menuitem
-	case 'navigation_menuitem_search':
+	case 'navigation_tree_search':
 		Action::write(function(&$response){
 			global $myUser,$_;
 			if (!$myUser->connected()) return;
-			if(!$myUser->can('navigation','read')) return;
 			require_once(__DIR__.SLASH.'MenuItem.class.php');
+			
+			if(!isset($_['menu']) || !is_numeric($_['menu'])) throw new Exception("Menu non spécifié");
 
-			$items = MenuItem::staticQuery('SELECT * FROM {{table}} WHERE user="" OR user=? ORDER BY sort',array($myUser->login),true);
-
-			$mainMenu = array();
-			Plugin::callHook("menu_main", array(&$mainMenu));
-			foreach ($mainMenu as $item) {
-				foreach ($items as $key=>$existingitem) {
-					if(isset($item['parent'])){
-						if(isset($existingitem->slug) && $item['parent'] == $existingitem->slug){
-							if(!isset($existingitem->childs)) $existingitem->childs = array();
-							$existingitem->childs[] = $item;
-							$items[$key] = $existingitem;
-						}
-					}
-				}
+			$response['rows'] = array();
+			$filters = array('menu'=>$_['menu']);
+			if(!$myUser->can('navigation','configure')) $filters['user'] = $myUser->login;
+			$items = array();
+
+			//recup des catégories principales
+			foreach(MenuItem::loadAll($filters,array('sort')) as $item){
+				$row = $item->toArray();
+				$row['class'] = $item->parent!=''  ? 'navigation-line-child' : '';
+				if(!$row['editable']) $row['class'] .= ' not-editable';
+				$row['target'] = MenuItem::target($item->target); 
+				$items[$row['id']] = $row;
+				if(empty($row['parent']))
+					$response['rows'][$row['id']] = $row;
 			}
-			foreach($items as $menuitem){
-				$response['rows'][] = $menuitem;
+
+			//Classement pas catégories
+			foreach ($items as $k=>$item) {
+				if(empty($item['parent'])) continue;
+				if(!isset($response['rows'][$item['parent']]['childs'])) $response['rows'][$item['parent']]['childs'] = array();
+				$response['rows'][$item['parent']]['childs'][] = $item;
 			}
-		});
-	break;
-	
-	case 'navigation_menuitem_move':
-		Action::write(function(&$response){
-			global $myUser,$_;
-			if(!$myUser->can('navigation','edit')) throw new Exception("Permissions insuffisantes",403);
-			require_once(__DIR__.SLASH.'MenuItem.class.php');
-			if(isset($_['sort']) && !empty($_['sort'])){
-				foreach ($_['sort'] as $sort => $id) {
-					$item = MenuItem::getById($id);
-					$item->sort = $sort;
-					$item->save();
-				}
+
+			//Tri des sous catégories par ordre de sort
+			foreach ($response['rows'] as $key=>$parent) {
+				if(!isset($response['rows'][$key]['childs'])) continue;
+				usort($response['rows'][$key]['childs'],function($a,$b){
+					if($a['sort'] > $b['sort']) return 1;
+					if($a['sort'] < $b['sort']) return -1;
+					if($a['sort'] == $b['sort']) return 0;
+				});
 			}
+			//le array value permet la réinitialisation des clés qui impactent sur l'ordre final
+			$response['rows'] = array_values($response['rows']);
+			
+
+			
 		});
 	break;
 
-	//Ajout ou modification d'élément menuitem
-	case 'navigation_menuitem_save':
+	//Sauvegarde des configurations de navigation
+	case 'navigation_setting_save':
 		Action::write(function(&$response){
 			global $myUser,$_,$conf;
-			if(!$myUser->can('navigation','edit')) throw new Exception("Permissions insuffisantes",403);
 			require_once(__DIR__.SLASH.'MenuItem.class.php');
-			if(!isset($_['url']) || empty($_['url'])) throw new Exception("Adresse web obligatoire");
-			if(!$conf->get('navigation_allow_custom_menu') && !$myUser->superadmin)  throw new Exception("La configuration du plugin n'autorise pas l'ajout de menu custom");
 
-			$item = MenuItem::provide();
-			if(!$item->editable && !$myUser->superadmin) throw new Exception("Ce menu n'est pas éditable");
+			if($conf->get("navigation_allow_custom_menu")==0 && !$myUser->can('navigation','configure')) throw new Exception("Error Processing Request", 403);
+
+			
+			if(!isset($_['menu']) || !is_numeric($_['menu'])) throw new Exception("Menu non spécifié");
+			
+			foreach(Configuration::setting('navigation') as $key=>$value){
+				if(!is_array($value)) continue;
+				$allowed[] = $key;
+			}
+			foreach ($_['fields'] as $key => $value)
+				if(in_array($key, $allowed)) $conf->put($key,$value);
+			
+
+			$filters = array('menu'=>$_['menu']);
+
+
+			if(!$myUser->can('navigation','configure')) $filters['user']=$myUser->login;
+			
+
+			if(!$myUser->superadmin) $filters['editable'] = '1';
+			MenuItem::delete($filters);
+
+			if(isset($_['items'])){
+				foreach ($_['items'] as $i => $item){
+					$menuItem = new MenuItem();
+
+					$menuItem->menu = $_['menu'];
+					$menuItem->icon = $item['icon'];
+					$menuItem->label = $item['label'];
+					if(isset($item['url'])) $menuItem->url = str_replace(ROOT_URL.'/','',$item['url']);
+					if(isset($item['target'])) $menuItem->target = $item['target'];
+					if(isset($item['visibility'])) $menuItem->visibility = $item['visibility'];
+					$menuItem->user = $myUser->can('navigation','configure') ? '' : $myUser->login;
+					$menuItem->slug = slugify($menuItem->label);
+					$menuItem->sort = $i;
+					if(MenuItem::rowCount(array('slug'=>$menuItem->slug)) <0 )
+						$item->slug .='_'.time();
+
+					$menuItem->save();
+
+					if(isset($item['childs'])){
+						foreach($item['childs'] as $u=>$subItem){
 
-			$item->icon = $_['icon'];
-			$item->label = $_['label'];
-			$item->url = str_replace(ROOT_URL.'/','',$_['url']);
-			$item->target = $_['target'];
+							$menuSubItem = new MenuItem();
+							$menuSubItem->menu = $_['menu'];
+							$menuSubItem->icon = $subItem['icon'];
+							$menuSubItem->label = $subItem['label'];
+							if(isset($subItem['url'])) $menuSubItem->url = str_replace(ROOT_URL.'/','',$subItem['url']);
+							if(isset($subItem['target'])) $menuSubItem->target = $subItem['target'];
+							if(isset($subItem['visibility'])) $menuSubItem->visibility = $subItem['visibility'];
+							$menuSubItem->user = $myUser->can('navigation','configure') ? '' : $myUser->login;
+							$menuSubItem->sort = $u;
+							$menuSubItem->slug = $menuItem->slug.'_'.slugify($menuItem->label);
 
-			$item->user = $myUser->login;
-			if(isset($_['public']) && $_['public']=='1' && $myUser->can('navigation','configure'))
-				$item->user = '';
+							if(MenuItem::rowCount(array('slug'=>$menuSubItem->slug)) <0 )
+								$menuSubItem->slug .='_'.time();
 
-			if($item->id==0){
-				$item->slug = slugify($item->label);
-				if(MenuItem::rowCount(array('slug'=>$item->slug)) <0 )
-					$item->slug .='_'.time();
+							$menuSubItem->parent = $menuItem->id;
+							$menuSubItem->save();
+						}
+					}
+				}
 			}
-			//$item->parent = $_['parent'];
-			$item->save();
+				
+
 		});
+			
 	break;
-	
-	//Récuperation ou edition d'élément menuitem
-	case 'navigation_menuitem_edit':
-		Action::write(function(&$response){
-			global $myUser,$_;
-			if(!$myUser->can('navigation','edit')) throw new Exception("Permissions insuffisantes",403);
-			require_once(__DIR__.SLASH.'MenuItem.class.php');
-			$response = MenuItem::getById($_['id']);
-		});
+	case 'navigation_widget_load':
+		global $myUser;
+		require_once(__DIR__.SLASH.'..'.SLASH.'dashboard'.SLASH.'DashboardWidget.class.php');
+		$widget = DashboardWidget::current();
+		if( $widget->data('title') == ""){
+			$widget->title =  'Bloc Menu';
+		}else{
+			$widget->title =  '';
+			$widget->icon = '';
+		}
+		
+		if($widget->data('color') != "") $widget->background = $widget->data('color');
+		ob_start();
+		require_once(__DIR__.SLASH.'widget.php');
+		$widget->content = ob_get_clean();
+		echo json_encode($widget);
 	break;
 
-	//Suppression d'élement menuitem
-	case 'navigation_menuitem_delete':
+	case 'navigation_widget_configure_save':
 		Action::write(function(&$response){
-			global $myUser,$_;
-			if(!$myUser->can('navigation','delete')) throw new Exception("Permissions insuffisantes",403);
-			require_once(__DIR__.SLASH.'MenuItem.class.php');
-			
-			MenuItem::deleteById($_['id']);
+		global $myUser,$_;
+		require_once(__DIR__.SLASH.'..'.SLASH.'dashboard'.SLASH.'DashboardWidget.class.php');
+		$widget = DashboardWidget::getById($_['id']);
 
-			// Décommenter pour une suppression logique
-			// $item = MenuItem::getById($_['id']);
-			// $item->state = MenuItem::INACTIVE;
-			// $item->save();
-		});
+		$url =  str_replace(ROOT_URL.'/','',$_['widget-url']);
+		$widget->data('icon',$_['widget-icon']);
+		$widget->data('title',$_['widget-title']);
+		$widget->data('color',$_['widget-color']);
+		$widget->data('url',$url);
+		$widget->save();
+	});
 	break;
 
-	//Sauvegarde des configurations de navigation
-	case 'navigation_setting_save':
+	case 'navigation_widget_configure':
+		global $myUser;
+		require_once(__DIR__.SLASH.'..'.SLASH.'dashboard'.SLASH.'DashboardWidget.class.php');
+		$widget = DashboardWidget::current();
+		ob_start();
+		require_once(__DIR__.SLASH.'widget.configure.php');
+		$content = ob_get_clean();
+		echo $content ;
+	break;
+
+	case 'navigation_widget_configure_autocomplete':
 		Action::write(function(&$response){
-			global $myUser,$_,$conf;
-			if(!$myUser->can('navigation','configure')) throw new Exception("Permissions insuffisantes",403);
-			foreach(Configuration::setting('navigation') as $key=>$value){
-				if(!is_array($value)) continue;
-				$allowed[] = $key;
-			}
-			foreach ($_['fields'] as $key => $value)
-				if(in_array($key, $allowed)) $conf->put($key,$value);
-		});
+		global $myUser,$_;
+		require_once(__DIR__.SLASH.'MenuItem.class.php');
+		if(!$myUser->connected()) throw new Exception("Vous devez être connecté",401);
+		$rows = array();
+		if($_['keyword'] == '') return;
+
+		
+		foreach(MenuItem::staticQuery('SELECT item.* FROM {{table}} item LEFT JOIN {{table}} menu ON item.menu=menu.id  WHERE item.label LIKE ? AND item.menu!=0 AND menu.user=?',array('%'.$_['keyword'].'%',$myUser->login),true) as $menu){
+			$row = $menu->toArray();
+			$row['name'] = $row['label'];
+			$rows[] = $row;
+		}
+
+		Plugin::callHook("menu_main", array(&$pages));
+		
+		
+		foreach($pages as $page){
+			
+			if( strpos(slugify($page['label']), slugify($_["keyword"])) === false ) continue;
+			$icon = isset($page['icon'])? $page['icon'] : 'far fa-bookmark';
+			$row =  $page;
+			$row['name'] = $page['label'];
+			$rows[] = $row;
+		}
+
+		usort ($rows , function($a,$b){return $a['sort']>$b['sort']?-1:1;});
+
+		$response['rows'] = $rows;
+	});
 	break;
 }
 ?>

+ 4 - 4
plugin/navigation/app.json

@@ -1,11 +1,11 @@
 {
-	"id": "fr.idleman.navigation",
+	"id": "fr.sys1.navigation",
 	"name": "Navigation",
 	"author" : {
-		"name" : ""
+		"name" : "Valentin CARRUESCO"
 	},
-	"version": "1.0",
-	"url": "http://idleman.fr",
+	"version": "2.0",
+	"url": "http://sys1.fr",
 	"licence": {"name": "Copyright","url" : ""},
 	"description": "Gestion des menus. Possibilité d'ajouter et de supprimer des éléments de menu personnalisables",
 	"require" : {}

+ 165 - 61
plugin/navigation/css/main.css

@@ -1,86 +1,190 @@
-.navigation-edit-button{
-	opacity: 0.5;
-	transition: all 0.2s ease-in-out;
+
+.navbar-main{
+	display: none;
 }
-.navigation-edit-button:hover{
-	opacity: 1;
+
+.module-navigation.page-iframe{
+	overflow:hidden;
 }
-.navigation-edit-item-button{
-	padding-left: 5px;
+
+.module-navigation.page-iframe,.module-navigation.page-iframe body,.module-navigation.page-iframe .container-fluid{
+	height:100%;
 }
-.navigation-edit-item-button i{
-	font-size: 0.75em;
-	transition: all 0.2s ease-in-out;
+.module-navigation.page-iframe .container-fluid{
+    padding: 50px 0px 0 0px;
 }
-.navigation-edit-item-button:hover i{
-	transform: scale(1.5);
+.module-navigation.page-iframe .footer{
+	display:none;
 }
-.navigation-edit-button > a,
-.navigation-validate-button > a,
-.navigation-plus-button > a {
-	text-align: right;
+.module-navigation.page-iframe body{
+	margin-bottom: 0px;
 }
-.navigation-plus-button{
-	opacity: 0.5;
-	transition : all 0.2s ease-in-out;
-	display: none;
+
+.navigation-tree{
+	background-color: #f8f8f8;
+	color:#333333;
 }
-.navigation-plus-button:hover{
-	opacity: 1;
+
+.navigation-menu-selector{
+	border: 0px;
+	background: transparent;
+	font-size: 16px;
 }
-.navigation-validate-button{
-	opacity: 0.5;
-	transition : all 0.2s ease-in-out;
-	display:none;
+
+.navigation-tree > h3{
+
+	color:#777777;
+}
+.navigation-tree .btn-info {
+    font-size: 11px;
+    text-transform: uppercase;
+    color: #ffffff;
+    margin-right:10px;
+    border:0;
+    padding:9px;
+    background-color: #4da3ff;
+    font-weight: bold;
+}
+.navigation-tree .btn-light {
+    font-size: 11px;
+    text-transform: uppercase;
+    color: #777777;
+     padding:9px;
+    border:0;
+    background-color: #ffffff;
+    font-weight: bold;
+}
+
+.navigation-tree ul,.navigation-tree li{
+	list-style-type: none;
+	padding: 0;
+	margin:0;
 }
-.navigation-validate-button:hover{
-	opacity: 1;
+
+.navigation-tree > ul{
+	margin:15px 0;
 }
-.navigation-sortable-placeholder{
-	width: 0px;
-	border: 2px solid #00b6ff;
+
+.navigation-tree .navigation-item{
+	background: #ffffff;
+	padding:10px;
 }
-.menuitem-form .navigation-options{
-	text-align: center;
-	font-size: 0.75em;
+
+.navigation-tree li.navigation-line{
+	margin-top: 5px;
 }
-.nav-option-container {
-	height: 20px;
-	width:  100%;
-	display:  flex;
-	margin: 10px 0;
+
+.navigation-tree li.navigation-line.not-editable{
+	    opacity: 0.6;
 }
-.nav-option-container > label {
-	flex: 1 0 auto;
-	text-align: left;
-	margin:  auto;
-	cursor:  pointer;
+.navigation-tree li.navigation-line.not-editable .navigation-item-header{
+	    cursor: default;
 }
-.nav-option-container > label.check-component {
-	flex: none;
-	margin-right: 5px;
+
+.navigation-tree li.navigation-line.not-editable .navigation-item-type{
+	display: none;
 }
-.nav-option-container > input {
-	width: auto;
-	margin: auto 10px;
-	cursor:  pointer;
+
+.navigation-tree .navigation-line .item-label{
+	border-left:0px;
 }
-.nav-option-container > input:focus {
-	box-shadow: none;
+
+.navigation-tree li.navigation-line-child{
+	margin-left: 35px;
 }
-.nav-select-container {
-	display: flex;
+
+
+.navigation-item-placeholder{
+	height: 40px; 
+	line-height: 1.2em;
+	border:2px dashed #cecece;
+	display: block;
 }
-.nav-select-container label {
-	margin: auto;
+
+.navigation-item-header{
+	cursor: move;
 }
-.nav-select-container select {
+
+.navigation-item-icon,.navigation-item-label,.navigation-item-url,.navigation-item-type{
 	display: inline-block;
-	width: 75%;
+	margin:0;
+	padding:0;
+	vertical-align: top;
 }
-.navbar-main {
-	display:none;
+
+.navigation-item-icon{
+	color: #222222;
+}
+
+.navigation-item-label{
+	margin: 2px 0 0 3px;
+	color: #222222;
+	font-weight: bold;
+	text-transform: uppercase;
+	font-size: 16px;
+}
+
+.navigation-item-url{
+	color: #aeaeae;
+	margin: 3px 0 0 5px;
+}
+.navigation-item-type{
+	color: #aeaeae;
+	text-transform: uppercase;
+	font-size: 14px;
+	float:right;
+	margin-top: 4px;
+}
+.navigation-item-toggle{
+	color: #aeaeae;
+	text-transform: uppercase;
+	font-size: 18px;
+	margin-left: 10px;
+	opacity: 0.7;
+	transition: all 0.2s ease-in-out;
+	cursor: pointer;
+}
+.navigation-item-toggle:hover{
+	opacity: 0.9;
+	transform: scale(1.3);
+}
+.navigation-item-form{
+	position: relative;
+	padding-bottom:40px;
 }
+.navigation-item-form > h4{
+	font-size: 14px;
+	font-weight: bold;
+	margin: 10px 0 3px 0;
+}
+
+.navigation-item-delete{
+	cursor: pointer;
+	position: absolute;
+	right: 5px;
+	bottom: 5px;
+	opacity: 0.7;
+	transition: all 0.2s ease-in-out;
+}
+.navigation-item-delete:hover{
+	opacity: 0.9;
+	transform: scale(1.3);
+}
+
+.navigation-item-form > form > label{
+	cursor: pointer;
+	padding:5px;
+}
+.navigation-item-form > form > label i{
+	color:#7d7d7d;
+	margin:0 6px;
+}
+
+
+.navigation-item .component-icon .dropdown-toggle {
+	border-radius: 3px 0 0 3px;
+}
+
 
 /* MEDIA QUERIES */
 @media (max-width: 1000px) {

+ 41 - 0
plugin/navigation/css/widget.css

@@ -0,0 +1,41 @@
+
+.widgetNavigationContainer h4.noContent{
+	text-align: center;
+	margin-top: 120px;
+	color:#cecece;
+}
+.widgetNavigationContainer{
+	display: block;
+	height: 100%;
+	width: 100%;
+	box-sizing: border-box;
+}
+.widgetNavigationContainer > a{
+	display: block;
+	height: 100%;
+	width: 100%;
+	text-align: center;
+	color:#ffffff;
+	text-transform: uppercase;
+	font-size: 25px;
+	padding:80px 15px 15px 15px;
+	text-decoration: none;
+	font-weight: 200;
+	box-sizing: border-box;
+	transition:all 0.2s linear;
+}
+.widgetNavigationContainer > a i{
+	display: block;
+	font-size: 40px;
+	margin:10px;
+	opacity: 0.8;
+	transition: transform 0.2s ease-in-out,opacity 0.2s linear;
+}
+
+.widgetNavigationContainer > a:hover  i{
+	opacity: 1;
+	transform: translateY(-10px);
+}
+.widgetNavigationContainer > a:hover{
+	text-shadow: 0px 0px 3px #ffffff;
+}

+ 175 - 100
plugin/navigation/js/main.js

@@ -1,135 +1,210 @@
-$(function(){
-	navigation_menuitem_search();
-})
+var navigationMenuEdited = false;
+
+//CHARGEMENT DE LA PAGE SETTING
+function init_setting_global_navigation(){
+
+	/*switch($.urlParam('page')){
+		default:
+		break;
+	}*/
+
+	window.onbeforeunload = function(e){
+	  if(!navigationMenuEdited) return e=null;
+	  return 'Vous n\'avez pas sauvgardé certaines modifications sur le menu, êtes vous sûr de vouloir quitter la page?';
+	  
+	};
+		
+
+	navigation_tree_search();
+	$( ".navigation-tree-list" ).sortable({
+		placeholder: "navigation-item-placeholder",
+		handle: ".navigation-item-header",
+		cancel: ".not-editable",
+		distance : 15,
+      	update : function(){
+      		navigationMenuEdited = true;
+      	},
+		grid: [ 50, 1 ],
+		sort: function(event,ui){
+			var leftLimit = ui.item.hasClass('navigation-line-child') ? -20: 15;
+			if(ui.position.left < leftLimit){
+				ui.helper.css('left',leftLimit+'px');
+			}
+
+			if(ui.position.left >65 ){
+				left = 65;
+				ui.helper.css('left',left+'px');
+			}
+			if(ui.item.index()==1){
+				ui.helper.css('left',leftLimit+'px');
+			}
+		},
+		stop: function(event,ui){
+
+
+			if(ui.position.left>15){
+				if(!ui.item.hasClass('navigation-line-child')) navigationMenuEdited = true;
+				ui.item.addClass('navigation-line-child');
+			}else{
+				if(ui.item.hasClass('navigation-line-child')) navigationMenuEdited = true;
+				ui.item.removeClass('navigation-line-child');
+			}
+
+		},
+		 helper: "clone",
+      opacity: 0.8
+    });
+    $( ".navigation-tree > u" ).disableSelection();
+
+	
+}
+
+
+
 
 //CHARGEMENT DE LA PAGE
+/*
 function init_plugin_navigation(){
 	switch($.urlParam('page')){
 		default:
 		break;
 	}
 }
+*/
+
+
+
+
+
+
+/** Editeur d'arborescence **/
 
 //Enregistrement des configurations
 function navigation_setting_save(){
+
+	var items = [];
+	var lastParentIndex = 0;
+	$('.navigation-line:visible').each(function(i,element){
+		var line = $(element);
+		var item = {};
+		item.label = line.find('.item-label').val();
+		item.visibility = line.find('input.item-visibility').val();
+		item.url = line.find('.item-url').val();
+		item.icon = line.find('.item-icon').val();
+		item.target = line.find('[name="item-target"]:checked').val();
+
+		if(line.hasClass('navigation-line-child')){
+			items[lastParentIndex].childs.push(item);
+		}else{
+			lastParentIndex = items.length;
+			item.childs = [];
+			items.push(item);
+		}
+
+	});
+	
+
 	$.action({ 
 		action : 'navigation_setting_save', 
-		fields :  $('#navigation-setting-form').toJson() 
+		fields :  $('#navigation-setting-form').toJson(),
+		items : items,
+		menu : $('#navigation-menu').val()
 	},function(){
+		navigationMenuEdited = false;
 		$.message('success','Enregistré');
 	});
 }
 
-/** MENUITEM **/
-//Récuperation d'une liste de menuitem dans le tableau #menuitems
-function navigation_menuitem_search(callback){
+
+function navigation_tree_search(callback){
+	var menu = $('#navigation-menu').val();
+	if(!menu || menu=='') return;
 	$.action({
-		action : 'navigation_menuitem_search'
+		action : 'navigation_tree_search',
+		menu : menu
 	},function(r){
-		$('.navigation-template-item:visible').remove();
-		for(var k in r.rows){
-			navigation_menuitem_add(r.rows[k]);
+		$('.navigation-tree > ul > li:visible').remove();
+		for(var key in r.rows){
+			var line =  r.rows[key];
+			navigation_tree_add(line);
+			if(line.childs){
+				for(var key2 in line.childs){
+					navigation_tree_add(line.childs[key2]);
+				}
+			}
 		}
-	});
-}
 
-//Ajout ou modification d'élément menuitem
-function navigation_menuitem_save(){
-	var data = $('#menuitem-form').toJson();
-	$.action(data,function(r){
-		$('#menuitem-form').attr('data-id','');
-		navigation_menuitem_search();
-		$('#quickform-modal').modal('hide');
-		$.message('success', 'Enregistré');
+		$('.navigation-line [name="item-target"]').each(function(){
+			var input = $(this);
+			if(input.attr('data-value')==input.val()) input.prop('checked', true);
+		});
+		init_components();
 	});
 }
 
-//Récuperation ou edition d'élément menuitem
-function navigation_menuitem_edit(element,event){
-	/*var line = $(element).closest('tr');
-	$.action({action:'navigation_menuitem_edit',id:line.attr('data-id')},function(r){
-		$.setForm('#menuitem-form',r);
-		$('#menuitem-form').attr('data-id',r.id);
-	});*/
-	var idMenu = $(element).attr('data-id');
-	var modal = $('#quickform-modal');
-	modal.find('#quickform-modal-label').text('Édition de menu');
-	modal.find('.modal-body').load('plugin/navigation/page.quick.item.php',{
-		id: idMenu
-	},function(){
-		var modalFooter = modal.find('.modal-footer');
-		var closeBtn = modalFooter.find('[data-dismiss="modal"]');
-		modalFooter.text('').append(closeBtn);
-		modalFooter.append('<div onclick="navigation_menuitem_save();" class="btn btn-success save-menu"><i class="fas fa-check"></i> Enregistrer</div>');
-		if(idMenu){
-			modalFooter.addClass('modal-footer-margin-auto');
-			modalFooter.prepend('<div onclick="navigation_menuitem_delete();" class="btn btn-danger delete-menu mr-auto"><i class="fas fa-times"></i> Supprimer</div>');
-		} else {
-			modalFooter.removeClass('modal-footer-margin-auto');
-		}
-		init_components(modal);
-	});
-	modal.modal('show');
-	if(!event)return;
-	event.preventDefault();
-	event.stopPropagation();
-}
 
-//Récuperation ou edition d'élément menuitem
-function navigation_navigation_edit(element){
-	$('.navigation-plus-button,.navigation-validate-button,.navigation-edit-item-button').show();
-	$('.navigation-edit-button').hide();
-	$('.navigation-menu').sortable({
-		items: 'li:not(.pointer)',
-		placeholder: "navigation-sortable-placeholder",
-		axis: "x",
-		cursor: "move",
-		stop : function(e, ui){
-			var sort = [];
-			$('.navigation-template-item:visible').each(function(i,element){
-				sort.push($(this).attr('data-id'));
-			});
-			$.action({
-				action : 'navigation_menuitem_move',
-				sort : sort
-			});
-		},
-		//handle: ".navigation-template-item"
-	});
+function navigation_tree_new(element){
+	var data = {
+		label : 'Sans titre',
+		icon : 'far fa-bookmark',
+		url : '',
+		labelType : 'non définis'
+	};
+
+
+	if(element){
+		data = $(element).closest('.list-group-item').data();
+		data.target = 'redirect';
+		$.message('info','Lien "'+data.label+'" ajouté au menu');
+	}
+
+	var line = navigation_tree_add(data);
+	$('.navigation-line .navigation-item-form').addClass('hidden');
+	line.find('.navigation-item-form').removeClass('hidden');
+	init_components();
+	line.find('.item-label').select();
+
 }
 
-function navigation_menuitem_add(data){
+function navigation_tree_add(data){
+	var tpl = $('.navigation-line:hidden').get(0).outerHTML;
+	tpl = tpl.replace('data-type="user-tpl"','data-type="user"');
+	var line = $(Mustache.render(tpl,data));
 
-	if(!data) data = {};
-	var data = $.extend({
-		label : 'Nouveau menu',
-		icon : 'user',
-	},data);
-	var tpl = $('.navigation-template-item.hide').get(0).outerHTML;
-	var item = $(Mustache.render(tpl,data));
+	line.removeClass('hidden');
 	
-	item.removeClass('hide');
-	$('.navigation-template-item.hide').before(item);	
+	$('.navigation-tree > ul').append(line);
+
+
+	line.find('[name="item-target"][value="'+data.target+'"]').prop('checked',true);
+
+	line.find('.item-label').keyup(function(){
+		navigationMenuEdited = true;
+		line.find('.navigation-item-label').text($(this).val());
+	});
+	line.find('.item-url').keyup(function(){
+		navigationMenuEdited = true;
+		line.find('.navigation-item-url').text($(this).val());
+	});
+	line.find('[name="item-target"]').click(function(){
+		navigationMenuEdited = true;
+		line.find('.navigation-item-type span').text($(this).parent().text());
+	});
+	line.find('.item-icon').change(function(){
+		navigationMenuEdited = true;
+		line.find('.navigation-item-icon i').attr('class',$(this).val());
+	});
+	return line;
 }
 
-function navigation_navigation_save(){
-	$('.navigation-plus-button,.navigation-edit-item-button,.navigation-validate-button').hide();
-	$('.navigation-edit-button').show();
-	$('.navigation-menu').sortable("disable");
+function navigation_tree_toggle(element){
+	var line = $(element).closest('.navigation-line');
+	$('.navigation-item-form',line).toggleClass('hidden');
 }
 
-//Suppression d'élement menuitem
-function navigation_menuitem_delete(element){
-	if(!confirm('Êtes vous sûr de vouloir supprimer ce menu ?')) return;
-	var line = $(element).closest('tr');
-	$.action({
-		action : 'navigation_menuitem_delete',
-		id : $('#menuitem-form').attr('data-id')
-	},function(r){
-		line.remove();
-		$('#menuitem-form').attr('data-id','');
-		navigation_menuitem_search();
-		$('#quickform-modal').modal('hide');
-		$.message('info', 'Menu supprimé');
-	});
+function navigation_tree_remove(element){
+	if(!confirm('Êtes-vous sûr de vouloir supprimer cet élement?')) return;
+	navigationMenuEdited = true;
+	$(element).closest('.navigation-line').remove()
 }
+

+ 45 - 0
plugin/navigation/js/widget.js

@@ -0,0 +1,45 @@
+
+
+
+
+function navigation_widget_configure_init(){
+	var widgetTitle = $('#widget-title');
+	$('#widget-title').autocomplete({
+					action : 'navigation_widget_configure_autocomplete',
+					skin : function(item){
+						var html = '';
+						var re = new RegExp(widgetTitle.val(),"gi");
+
+						name = item.name.replace(re, function (x) {
+							return '<strong>'+x+'</strong>';
+						});
+						
+				
+						html += '<h5 class="mt-1"><i class="'+item.icon+'"></i> <span>'+name+'</span></h5>'; 
+						html +='<small>'+item.url+'</small></div>';
+						html += '<div class="clear"></div>';
+						
+						
+						return html;
+					},
+					highlight : function(item){
+						return item;
+					},
+					onClick : function(selected,element){
+						widgetTitle.val(selected.name);
+						$('#widget-url').val(selected.url);
+						$('#widget-icon').val(selected.icon);
+						init_components();
+					}
+	});
+}
+
+function navigation_widget_configure_save(widget,modal){
+	var data = $('#navigation-widget-form').toJson();
+	data.action = 'navigation_widget_configure_save';
+	data.id = modal.attr('data-widget');
+	$.action(data,function(){
+		$.message('success','Configuration enregistrée');
+		dashboard_dashboardwidget_search();
+	});
+}

+ 194 - 133
plugin/navigation/navigation.plugin.php

@@ -1,141 +1,137 @@
 <?php
 //Déclaration d'un item de menu dans le menu principal
 function navigation_menu(){
-	global $_,$myUser,$conf;
-	// if(!$myUser->can('navigation','read')) return;
-	$page = basename($_SERVER['REQUEST_URI']);
-	$menuItems = array();
-
-	$menuItems[] = array(
-		'sort'=>299,
-		'id'=>'{{id}}',
-		'url'=>'{{url}}',
-		'title'=>'{{title}}',
-		'target'=>'{{target}}',
-		'label'=>'{{{label}}}',
-		'icon'=> '{{icon}}',
-		'classes' =>'navigation-template-item hide',
-		'html' =>'<span class="navigation-edit-item-button hide" data-id="{{id}}" onclick="navigation_menuitem_edit(this,event)"><i class="fas fa-pencil-alt"></i></span>'
-	);
-
-	$menuItems[] = array(
-		'sort'=>400,
-		'label'=>'Affaires',
-		'slug'=>'case',
-		'icon'=> 'fas fa-suitcase',
-		'classes' =>'pointer'
-	);
-	$menuItems[] = array(
-		'sort'=>401,
-		'label'=>'Organisation',
-		'slug'=>'organization',
-		'icon'=> 'far fa-calendar-alt',
-		'classes' =>'pointer'
-	);
-	$menuItems[] = array(
-		'sort'=>402,
-		'label'=>'Statistiques',
-		'slug'=>'statistics',
-		'icon'=> 'fas fa-chart-line',
-		'classes' =>'pointer',
-	);
-	$menuItems[] = array(
-		'sort'=>403,
-		'label'=>'Exploitation',
-		'slug'=>'exploitation',
-		'icon'=> 'fas fa-cubes',
-		'classes' =>'pointer'
-	);
-	uasort($menuItems , function($a,$b){return $a['sort']>$b['sort']?1:-1;});
-
-	//Menus ajoutés par les plugins
-	$mainMenu = array();
-	Plugin::callHook("menu_main", array(&$mainMenu));
-	uasort ($mainMenu , function($a,$b){return $a['sort']>$b['sort']?1:-1;});
-
-	foreach ($mainMenu as $item) {
-		foreach ($menuItems as $key=>$existingitem) {
-			if(!isset($item['parent'])) continue;
-			if(!isset($existingitem['slug']) || $item['parent'] != $existingitem['slug']) continue;
-			if(!isset($existingitem['childs'])) 
-				$existingitem['childs'] = array();
-			$existingitem['childs'][] = $item;
-			$menuItems[$key] = $existingitem;
-		}
-	}
-
-	if($conf->get('navigation_show_uncategorized')){
-		foreach ($mainMenu as $item)
-			if(!isset($item['parent'])) $menuItems[] = $item;
-	}
-
-	if($myUser->can('navigation','edit')){
-		$menuItems[] = array(
-			'sort'=>300,
-			'onclick'=>'navigation_navigation_edit()',
-			'title'=>'Modifier le menu',
-			'icon'=> 'fas fa-edit',
-			'classes' =>'navigation-edit-button pointer' 
-		);
-		$menuItems[] = array(
-			'sort'=>300,
-			'onclick'=>'navigation_navigation_save()',
-			'title'=>'Arrêter les modifications',
-			'icon'=> 'fas fa-check',
-			'classes' =>'navigation-validate-button pointer' 
-		);
-		$menuItems[] = array(
-			'sort'=>300,
-			'onclick'=>'navigation_menuitem_edit()',
-			'title'=>'Ajouter un item',
-			'icon'=> 'fas fa-plus',
-			'classes' =>'navigation-plus-button pointer' 
-		);
-	}
+	require_once(__DIR__.SLASH.'MenuItem.class.php');
+	global $myUser,$conf;
  ?>
 	<ul class="navbar-nav navigation-menu">
 	<?php if ($myUser->connected()): ?>
-		<?php foreach($menuItems as $item): ?>
-			<?php if (isset($item['label']) && $item['label'] == 'Réglages') $page = basename($_SERVER['PHP_SELF']); 
-			$classes = isset($item['classes'])? $item['classes']: '';
-			if( isset($item['url']) && (
-				(!empty($item['url']) 
-				&& strpos($page, $item['url']) !== false 
-				&& $item['url'] != 'index.php') 
-				|| $page == $item['url'])
-			){
-				$classes .= ' active';
-			} 
-
-			if(!isset($item['childs']) && empty($item['url']) && empty($item['onclick'])) continue;
-			?>
-			
-			<li data-id="<?php echo isset($item['id'])?$item['id']:''; ?>" class="nav-item <?php echo $classes; ?>  <?php echo isset($item['childs'])? 'dropdown':''; ?>">
-				<a class="nav-link<?php echo isset($item['childs'])? ' dropdown-toggle':''?>"  
-				<?php echo isset($item['childs'])? 'data-toggle="dropdown"':''; ?>
-				title="<?php echo isset($item['title'])?$item['title']:''; ?>" 
-				target="<?php echo isset($item['target'])?$item['target']:''; ?>" 
-				onclick="<?php echo isset($item['onclick'])?$item['onclick']:''; ?>" 
-				<?php echo isset($item['url'])? 'href="'.$item['url'].'"':''; ?>>
-				<?php echo (isset($item['icon'])?'<i class="'.$item['icon'].'"></i> ':'').'<span>'.(isset($item['label'])?$item['label']:'').'</span>'; ?>
-				<?php echo isset($item['html'])? $item['html']:''; ?>
-				</a>
-				<?php if(isset($item['childs'])): ?>
-				 <div class="dropdown-menu" aria-labelledby="navbarDropdown">
-				 	<?php foreach($item['childs'] as $child): ?>
-			      		<a class="dropdown-item" href="<?php echo $child['url']; ?>">
-							<?php echo (isset($child['icon'])?'<i class="'.$child['icon'].'"></i> ':'') ?>
-			      			<?php echo $child['label']; ?></a>
-			    	<?php endforeach; ?>
-			    </div>
-			    <?php endif; ?>
-			</li>
-		<?php endforeach; ?>
+		<?php foreach(MenuItem::bySlug('main-menu','') as $item): ?>
+			<?php 
+			navigation_item_template($item);
+		  endforeach; ?>
+	<?php endif; ?>
+	</ul>
+
+	<?php if($conf->get("navigation_allow_custom_menu")==0) return; ?>
+	<ul class="navbar-nav navigation-shortcut-menu">
+	<?php if ($myUser->connected()): ?>
+		<?php foreach(MenuItem::bySlug('shortcut-menu',$myUser->login) as $item): ?>
+			<?php 
+			navigation_item_template($item);
+		  endforeach; ?>
 	<?php endif; ?>
 	</ul>
 	<?php
 }
 
+function navigation_item_template($item){
+		global $myUser,$myFirm,$conf;
+		$page = basename($_SERVER['REQUEST_URI']);
+
+
+		$id = isset($item['id'])?$item['id']:'';
+		$target = isset($item['target'])?$item['target']:'';
+		$url = isset($item['url'])?$item['url']:'';
+		$label = isset($item['label'])?$item['label']:'';
+		$html = isset($item['html'])?$item['html']:'';
+
+		if (isset($item['label']) && $item['label'] == 'Réglages') $page = basename($_SERVER['PHP_SELF']); 
+
+		$linkClasses = '';
+		$classes = isset($item['classes'])? $item['classes']: '';
+
+
+		if(isset($item['childs']) ){
+			$linkClasses .= ' nav-link';
+			if(count($item['childs'])!=0){
+				$classes .= ' dropdown';
+				$url = '';
+				$linkClasses .= ' nav-link dropdown-toggle';
+			}
+
+			//
+			$mustShow = false;
+			foreach ($item['childs'] as $child) {
+				//Vérification des droits de visibilité sur le menu
+				$childObject = new MenuItem();
+				$childObject->fromArray($child);
+				if($childObject->visibleFor($myUser)){
+					$mustShow = true;
+					break;
+				}
+			}
+
+			if(!$mustShow && $item['url']=='') return;
+		}
+		if( $item['parent'] && $item['parent']!='') $linkClasses .= 'dropdown-item ';
+
+
+		$url = navigation_meta_link($url);
+		$label = navigation_meta_link($label);
+		$html = navigation_meta_link($html);
+
+		$icon = isset($item['icon'])?'<i class="'.$item['icon'].'"></i> ':'';
+		
+		
+		if($target=='iframe'){
+			$target = '';
+			$url = 'index.php?module=navigation&page=iframe&url='.base64_encode($url);
+		}
+		if($target=='redirect')
+			$target = '';
+		
+
+		//Highlight du menu sélectionné
+		if( $url !='' && ((strpos($page,  $url) !== false &&  $url != 'index.php') || $page ==  $url))
+			$classes .= ' active';
+		
+		
+
+		//Vérification des droits de visibilité sur le menu
+		$itemObject = new MenuItem();
+		$itemObject->fromArray($item);
+		if(!$itemObject->visibleFor($myUser)) return;
+
+		//si les logs sont activés on desactive le direct link pour passer par la surcouche de logs
+		if($conf->get('navigation_enable_log')) if(!empty($url)) $url = 'action.php?action=navigation_redirect&url='.base64_encode($url);
+		
+
+	?>
+
+	<li data-id="<?php echo $id; ?>" class="nav-item <?php echo $classes; ?>">
+		<a class="<?php echo $linkClasses; ?>"  
+		<?php echo isset($item['childs']) && count($item['childs'])!=0? 'data-toggle="dropdown"':''; ?>
+		title="<?php echo isset($item['title'])?$item['title']:''; ?>" 
+		target="<?php echo $target; ?>" 
+		onclick="<?php echo isset($item['onclick'])?$item['onclick']:''; ?>" 
+		href="<?php echo $url; ?>" >
+			<?php echo $icon; ?><?php echo $label; ?>
+			<?php echo $html; ?>
+		</a>
+		<?php if(isset($item['childs'])): ?>
+		 <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
+		 	<?php 
+		 	foreach($item['childs'] as $child): 
+		 		navigation_item_template($child);
+		 	 endforeach; 
+		 	?>
+	    </ul>
+	    <?php endif; ?>
+	</li>
+
+	<?php
+}
+
+//Application des données de templates sur l'url, libellé...
+function navigation_meta_link($string){
+	global $myUser;
+	$data = array('user'=>$myUser->toArray());
+	$data['user']['meta'] = $myUser->meta;
+	unset($data['password']);
+	return template($string,$data);
+}
+
 //Cette fonction va generer une page quand on clique sur navigation dans menu
 function navigation_page(){
 	global $_,$myUser;
@@ -149,14 +145,43 @@ function navigation_page(){
 
 //Fonction executée lors de l'activation du plugin
 function navigation_install($id){
-	if($id != 'fr.idleman.navigation') return;
+	if($id != 'fr.sys1.navigation') return;
 	Entity::install(__DIR__);
+	require_once(__DIR__.SLASH.'MenuItem.class.php');
+	
+	$mainMenu = new MenuItem();
+	$mainMenu->label = 'Menu principal';
+	$mainMenu->slug = 'main-menu';
+	$mainMenu->user = '';
+	$mainMenu->menu = 0;
+	$mainMenu->editable = 0;
+	$mainMenu->save();
+
 
+	$pages = array();
+	Plugin::callHook("menu_main", array(&$pages));
+	uasort ($pages , function($a,$b){return $a['sort']>$b['sort']?1:-1;});
+
+	
+	foreach($pages as $i=>$page): 
+		$icon = isset($page['icon'])? $page['icon'] : 'far fa-bookmark';
+	
+		$menuItem = new MenuItem();
+		$menuItem->label = $page['label'];
+		$menuItem->icon = $icon;
+		$menuItem->url = $page['url'];
+		$menuItem->user = '';
+		$menuItem->menu = 0;
+		$menuItem->sort = $i;
+		$menuItem->slug = $mainMenu->slug.'_'.slugify($menuItem->label);
+		$menuItem->menu = $mainMenu->id;
+		$menuItem->save();
+	endforeach; 
 }
 
 //Fonction executée lors de la désactivation du plugin
 function navigation_uninstall($id){
-	if($id != 'fr.idleman.navigation') return;
+	if($id != 'fr.sys1.navigation') return;
 	Entity::uninstall(__DIR__);
 }
 
@@ -194,15 +219,51 @@ function navigation_content_setting(){
 //Types possibles : text,select ( + "values"=> array('1'=>'Val 1'),password,checkbox. Un simple string définit une catégorie.
 Configuration::setting('navigation',array(
     "Général",
-    'navigation_show_uncategorized' => array("label"=>"Afficher les menus natifs non catégorisés","legend"=>"Affiche les menus de plugins qui n'ont pas de catégorie parente","type"=>"checkbox"),
-    'navigation_allow_custom_menu' => array("label"=>"Autoriser la création de menu personnalisés","legend"=>"Autorise aux utilisateurs la création de menu custom dans la navbar","type"=>"checkbox"),
+    'navigation_allow_custom_menu' => array("label"=>"Autoriser la création de menu personnalisés","legend"=>"Autorise aux utilisateurs la création de raccourcis dans la barre principale","type"=>"checkbox"),
+    'navigation_enable_log' => array("label"=>"Activer les logs au clic sur un menu","legend"=>"Cette option peut considérablement augmenter la taille de la base de données","type"=>"checkbox"),
 ));
 
 //Déclation des assets
-Plugin::addCss("/css/main.css?v=1"); 
-Plugin::addJs("/js/main.js?v=1"); 
+Plugin::addCss("/css/main.css"); 
+Plugin::addJs("/js/jquery.nestable.js"); 
+Plugin::addJs("/js/main.js"); 
+
+Plugin::addHook("menu_user", function(&$userMenu){
+	global $conf;
+	if($conf->get("navigation_allow_custom_menu")==0) return;
+	$userMenu[]= array(
+		'sort' =>0.5,
+		'label' => 'Mes Raccourcis',
+		'icon' => 'far fa-star',
+		'url' => 'setting.php?section=global.navigation&menu=mine'
+	);
+
+});
+
+
+
+function navigation_widget(&$widgets){
+	global $myUser;
+	require_once(__DIR__.SLASH.'..'.SLASH.'dashboard'.SLASH.'DashboardWidget.class.php');
+
+	$modelWidget = new DashboardWidget();
+	$modelWidget->model = 'menu';
+	$modelWidget->title = 'Menu';
+	$modelWidget->icon = 'far fa-compass';
+	$modelWidget->background = '#130f40';
+	$modelWidget->load = 'action.php?action=navigation_widget_load';
+	$modelWidget->js = [Plugin::url().'/js/widget.js?v=0'];
+	$modelWidget->css = [Plugin::url().'/css/widget.css?v=0'];
+	$modelWidget->configure = 'action.php?action=navigation_widget_configure';
+	$modelWidget->configure_callback = 'navigation_widget_configure_save';
+	$modelWidget->configure_init = 'navigation_widget_configure_init';
+	$modelWidget->description = "Affiche un menu sélectionné";
+	$widgets[] = $modelWidget;
+}
+
 
 //Mapping hook / fonctions
+Plugin::addHook("widget", "navigation_widget");
 Plugin::addHook("install", "navigation_install");
 Plugin::addHook("uninstall", "navigation_uninstall"); 
 Plugin::addHook("section", "navigation_section");

+ 76 - 0
plugin/navigation/page.iframe.php

@@ -0,0 +1,76 @@
+<?php 
+
+global $_,$myUser;
+
+if(!$myUser->connected()) throw new Exception("Connexion requise", 401);
+if(!isset($_['url'])) throw new Exception("Url d'entrée non définie");
+
+
+$url = base64_decode($_['url']);
+
+	try{
+
+		$request = @get_headers($url, true);
+		
+		$error = '';
+		if(!$request) $response['state'] = false; 
+
+		if(is_array($request) && isset($request[0])){
+			$headers = array();
+			// Make more sane response
+		    foreach($request as $h => $v)
+		    {
+		        if(is_int($h))
+		            $headers[$h]['Status'] = $v;
+		        else
+		        {
+		            if(is_string($v))
+		                $headers[0][$h] = $v;
+		        }
+		    }
+		    $lastHeader = end($headers);
+
+			if(!preg_match('/ 200 /i',$lastHeader['Status'])) throw new Exception($lastHeader['Status']);
+				
+			
+		}else{
+			throw new Exception('HTTP/1.1 404 Not Found'); 
+		}
+
+	}catch(Exception $e){
+		$code = 0;
+
+		list($protocol,$code,$sentence) = explode(' ',$e->getMessage()); 
+		
+
+		$mapping = array(
+			400 => 'La syntaxe de la requête est erronée.',
+			401 => 'Utilisateur non authentifié',
+			402 => 'Paiement requis pour accéder à la ressource.',
+			403 => 'Accès refusé',
+			404 => 'Adresse introuvable ou momentanément indisponible',
+			495 => 'Problème de certificat SSL',
+			498 => 'Le jeton a expiré ou est invalide.',
+			500 => 'Le serveur rencontre des erreurs internes',
+			502 => 'En agissant en tant que serveur proxy ou passerelle, le serveur a reçu une réponse invalide depuis le serveur distant.',
+			503 => 'Service temporairement indisponible ou en maintenance.',
+		);
+		if(isset($mapping[$code])){
+			$error = $mapping[$code];
+		}else{
+			$error = 'raison indéfinie ('.$code.' - '.$sentence.')';
+		}
+
+	}
+	
+if(!empty($error)){
+	?>
+	<div class="alert alert-warning m-3" role="alert">
+  		<strong>Erreur</strong> Le portail <a href="<?php echo $url; ?>" class="alert-link"><?php echo $url; ?></a> n'est pas accessible. Raison : <?php echo $error; ?>
+	</div>
+
+	<?php
+}else{
+?>
+<iframe src="<?php echo $url; ?>" style="width:100%;height:100%;border:0" frameborder="0"></iframe>
+<?php } ?>

+ 141 - 2
plugin/navigation/setting.global.navigation.php

@@ -1,7 +1,11 @@
 <?php
 global $myUser,$conf;
 if(!$myUser->connected()) throw new Exception("Vous devez être connecté pour accéder à cette fonctionnalité",401);
-if(!$myUser->can('navigation','configure')) throw new Exception("Vous n'avez pas la permission pour executer cette fonctionnalité",403);
+
+require_once(__DIR__.SLASH.'MenuItem.class.php');
+
+
+if($conf->get("navigation_allow_custom_menu")==0 && !$myUser->can('navigation','configure')) throw new Exception("Vous n'avez pas la permission d'afficher cette page", 403);
 ?>
 
 <div class="row">
@@ -11,7 +15,142 @@ if(!$myUser->can('navigation','configure')) throw new Exception("Vous n'avez pas
         <h3>Réglages Navigation</h3>
         <hr>
 	</div>
-	<div class="col-md-12">
+	
+
+	<div class="col-md-12 navigation-tree">
+		<br>
+
+        <h3>Arborescence du menu 
+        	<select class="navigation-menu-selector text-primary" id="navigation-menu" onchange="navigation_tree_search()">
+        		<?php 
+
+        		$filters = array('menu'=>'0');
+        		if($myUser->can('navigation','configure')){
+        			//l'admin menu voit ses menu et les menu non attaché a un user
+        			$filters['user:IN'] = ','.$myUser->login;
+        		}else{
+        			//les non admins menu ne voient que leurs menus
+        			$filters['user'] = $myUser->login;
+        		}
+        		$menus = MenuItem::loadAll($filters,array('label'));
+
+                //Récuperation du menu raccourcis du user
+                $shortcutMenu = MenuItem::load(array('user'=>$myUser->login,'slug'=>'shortcut-menu'));
+        		//On créé un menu raccourcis pour l'utilisateur courant si celui ci n'existe pas
+        		if(!$shortcutMenu){
+        			$shortcutMenu = new MenuItem();
+        			$shortcutMenu->label = 'Menu raccourcis';
+        			$shortcutMenu->slug = 'shortcut-menu';
+        			$shortcutMenu->user = $myUser->login;
+        			$shortcutMenu->menu = '0';
+        			$shortcutMenu->save();
+        			$menus[] = $shortcutMenu;
+        		}
+        		foreach($menus as $menu): ?>
+        			<option <?php echo (isset($_['menu']) && $_['menu']=="mine" &&  $shortcutMenu->id == $menu->id ? 'selected="selected"': ''); ?> value="<?php echo $menu->id ?>"><?php echo $menu->label ?></option>
+        		<?php endforeach; ?>
+        	</select>
+
+        </h3>
+        <hr>
+        <div onclick="navigation_tree_new();" class="btn btn-info"><i class="fas fa-plus"></i> Nouvel élément</div>
+        <div class="btn btn-light" data-toggle="modal" data-target="#navigation-page-modal"><i class="fas fa-plus"></i> Ajouter une page existante</div>
+     
+
+        <ul class="navigation-tree-list">
+        	<li class="navigation-line {{class}} hidden" data-id="{{id}}" >
+
+        		<div class="navigation-item">
+        			<div class="navigation-item-header">
+	        			<span class="navigation-item-icon"><i class="{{icon}}"></i></span> 
+	        			<h1 class="navigation-item-label">{{label}}</h1> 
+	        			<small class="navigation-item-url">{{#url}}- {{url}}{{/url}}</small> 
+	        		
+	        		
+	        		<h3 class="navigation-item-type"><span>{{target.label}}</span> <i  onclick="navigation_tree_toggle(this)" title="Afficher/Cacher" class="navigation-item-toggle fas fa-angle-right"></i></h3> 
+        			</div>
+
+					<div class="navigation-item-form hidden">
+
+
+						<h4>Titre</h4>
+						<div class="input-group mb-3">
+							<input type="text" class="item-icon" data-type="icon" value="{{icon}}">
+							<input type="text" class="form-control item-label" value="{{label}}" placeholder="Libellé du lien">
+						</div>
+
+						<h4>Adresse <small class="text-muted">- Optionnel</small></h4>
+						<div class="input-group mb-3">
+							<input type="text" class="form-control item-url" value="{{url}}"  placeholder="Adresse du lien">
+						</div>
+						<h4>Type d'ouverture <small class="text-muted">- Optionnel</small></h4>
+						<!-- la balise form est obligatoire pour contrer le bug de jquery ui sortable, ne pas enlever -->
+						<form>
+							<?php foreach (MenuItem::target() as $slug => $target): ?>
+							<label><input type="radio" name="item-target" value="<?php echo $slug; ?>" data-value="{{target.slug}}"> <i class="<?php echo $target['icon']; ?>"></i> <?php echo $target['label']; ?></label>
+							<?php endforeach;  ?>
+						</form>
+
+						<h4>Visibilité <small class="text-muted">- Laisser vide pour rendre public</small></h4>
+						<input type="text" class="item-visibility form-control" data-type="user-tpl" data-types="rank,user" data-multiple="true" value="{{visibility}}">
+		
+
+						<i title="Supprimer" class="navigation-item-delete far fa-trash-alt" onclick="navigation_tree_remove(this)"></i>
+					</div>
+        		</div>
+        		<ul></ul>
+        	</li>
+        </ul>
+ 	</div>
+
+	<?php if($myUser->can('navigation','configure')): ?>
+	<div class="col-md-12 mt-3">
+		<h3>Options générales</h3>
 		<?php echo Configuration::html('navigation'); ?>
 	</div>
+	<?php endif; ?>
+
+
+
 </div>
+
+
+<!-- Modale page interne -->
+<div class="modal fade" id="navigation-page-modal" tabindex="-1" role="dialog" aria-hidden="true">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h5 class="modal-title" id="exampleModalLabel">Ajouter une page</h5>
+        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+          <span aria-hidden="true">&times;</span>
+        </button>
+      </div>
+      <div class="modal-body">
+        <ul class="list-group">
+		<?php 
+
+		$pages = array();
+		Plugin::callHook("menu_main", array(&$pages));
+		uasort ($pages , function($a,$b){return $a['sort']>$b['sort']?1:-1;});
+	
+		
+		foreach($pages as $page): 
+			$icon = isset($page['icon'])? $page['icon'] : 'far fa-bookmark';
+		?>
+		<li class="list-group-item d-flex justify-content-between align-items-center" data-label="<?php echo $page['label']; ?>"  data-icon="<?php echo $page['icon']; ?>" data-url="<?php echo $page['url']; ?>">
+			<div>
+			<i class="<?php echo $icon; ?>"></i> <?php echo $page['label']; ?> <a class="text-muted" href="<?php echo $page['url']; ?>"> -<?php echo $page['url']; ?></a>
+			</div>
+			<span class="badge badge-success pointer" title="Ajouter au menu" onclick="navigation_tree_new(this);"><i class="fas fa-plus"></i></span>
+		</li>
+
+		<?php endforeach; ?>
+		</ul>
+
+      </div>
+      <div class="modal-footer">
+        <button type="button" class="btn btn-secondary" data-dismiss="modal">Fermer</button>
+      </div>
+    </div>
+  </div>
+</div>

+ 10 - 0
plugin/navigation/widget.configure.php

@@ -0,0 +1,10 @@
+<div id="navigation-widget-form">
+	Icone / Titre / couleur
+	<div class="input-group mb-2">
+		<input class="form-control" type="text" data-type="icon" value="<?php echo $widget->data('icon') =='' ? 'far fa-bookmark':$widget->data('icon');  ?>" id="widget-icon">
+		<input class="form-control" type="text" value="<?php echo $widget->data('title'); ?>" id="widget-title">
+		<input class="form-control pointer" style="max-width: 50px;" type="color" value="<?php echo $widget->data('color'); ?>" id="widget-color">
+	</div>
+	Adresse
+	<input class="form-control" type="text" value="<?php echo $widget->data('url'); ?>" id="widget-url">
+</div> 

+ 25 - 0
plugin/navigation/widget.php

@@ -0,0 +1,25 @@
+<?php 
+global $myUser;
+
+
+
+
+$icon = $widget->data('icon') == '' ? 'far fa-bookmark' : $widget->data('icon');
+$color = $widget->data('color') =='' ? '#ffffff': $widget->data('color');
+
+
+$url = navigation_meta_link($widget->data('url'));
+$title = navigation_meta_link($widget->data('title'));
+
+?>
+<div style="background: <?php echo $color; ?>" class="widgetNavigationContainer">
+	<?php  if(!empty($widget->data('title'))): ?>
+		<a href="<?php echo $url; ?>">
+			<i class="<?php echo $icon; ?>"></i>
+			<?php echo $title; ?>
+		</a>
+	<?php else: ?>
+		<h4 class="noContent"><i class="far fa-compass"></i> Aucun menu spécifié</h4>
+	<?php endif; ?>
+
+</div>

+ 75 - 39
plugin/notification/Notification.class.php

@@ -6,7 +6,7 @@
  * @license copyright
  */
 class Notification extends Entity{
-	public $id,$label,$html,$type,$meta,$end,$start;
+	public $id,$label,$html,$type,$meta,$end,$start,$pinned;
 	protected $TABLE_NAME = 'notification';
 	public $fields =
 	array(
@@ -16,9 +16,15 @@ class Notification extends Entity{
 		'type' => 'string',
 		'meta' => 'longstring',
 		'start' => 'date',
-		'end' => 'date'
+		'end' => 'date',
+		'pinned' => 'int'
 	);
 
+	function __construct(){
+ 		parent::__construct();
+ 		$this->pinned = 0;
+	}
+
 	public static function types($key = null){
 		$types = array(
 			'notice' => array(
@@ -26,12 +32,30 @@ class Notification extends Entity{
 				'color' =>'#007fdc',
 				'icon'  =>'far fa-flag',
 				'description'  =>"Notifications d'informations mineures",
+				'default_methods' => array(
+					'interface' => true,
+					'mail' => false
+				)
+			),
+			'announcement' => array(
+				'label' =>'Annonce',
+				'color' =>'#30336b',
+				'icon'  =>'fas fa-bullhorn',
+				'description'  =>"Annonce générale",
+				'default_methods' => array(
+					'interface' => true,
+					'mail' => false
+				)
 			),
 			'alert' => array(
 				'label' =>'Alerte',
 				'color' =>'#ff3100',
 				'icon'  =>'fas fa-fire',
 				'description'  =>"Notifications importantes ou critiques",
+				'default_methods' => array(
+					'interface' => true,
+					'mail' => false
+				)
 			)
 		);
 
@@ -40,64 +64,43 @@ class Notification extends Entity{
 		return $key ? $types[$key] : $types; 
 	}
 
-	public static function emit($infos,$recipients = array()){
-		require_once(__DIR__.SLASH.'UserNotification.class.php');
-
-		//Nettoyage des notifications périmées
-		self::clear();
+	public static function emit($infos){
+		$sendTypes = array();
+		Plugin::callHook("notification_methods", array(&$sendTypes));
 
 		$default = array(
 			'label' => "Notification sans titre",
 			'html' => "",
 			'type' => "notice",
+			'pinned' => 0,
 			'meta' => array(),
 			'start' => time(),
-			'end' => strtotime('+3month')
+			'end' => strtotime('+3month'),
+			'recipients' => array()
 		);
-
 		$infos = array_merge($default,$infos);
 
 		$notification = new Notification();
 		$notification->label = $infos['label'];
 		$notification->html = $infos['html'];
 		$notification->type = $infos['type'];
+		$notification->pinned = $infos['pinned'];
 		$notification->meta = json_encode($infos['meta']);
 		$notification->end = $infos['end'];
 		$notification->start = $infos['start'];
 		$notification->save();
-			
-		foreach($recipients as $recipient) {
-			$userNotification = new UserNotification();
-			$userNotification->notification = $notification->id;
-			$userNotification->user = $recipient;
-			$userNotification->read = 0;
-			$userNotification->save();
-		}
 
-		$preferences = array();
-		foreach(UserPreference::staticQuery("SELECT * FROM {{table}} up WHERE user IN ('".implode("','",$recipients)."') AND up.key IN('notification_send_mail','notification_categories')",array(),true) as $row){
-			if(!isset($preferences[$row->user])) $preferences[$row->user] = array();
-			$preferences[$row->user][$row->key] = $row->value;
-		}
 
-		foreach($preferences as $user => $preference){
-			
-			if(trim($preference['notification_send_mail']) == '') continue;
-			if(trim($preference['notification_categories']) == '') continue;
-			
-			$categories = json_decode($preference['notification_categories'],true);
-			
-			if(!is_array($categories) || !in_array($notification->type, $categories)) continue;
-
-			$mail = new Mail();
-			$mail->expeditor = 'IdleCorp <no-reply@idleman.fr>';
-			$mail->title = $notification->label;
-			$mail->recipients['to'][] = $preference['notification_send_mail'];
-			$mail->message = $notification->html;
-			$mail->message .= '<br><a href="'.ROOT_URL.'/index.php?module=notification#'.$userNotification->id.'">Voir la notification</a>';
-			$mail->send();
+		// pour chaque destinataire, vérification des préférences
+		foreach ($infos['recipients'] as $recipient) {
+			//$userPreferences = Notification::getNotificationPreferences($recipient);
+			$userPreferences = Notification::settingPreferences($recipient);
+			// pour chaque type d'envoi
+			foreach ($sendTypes as $sendType) {
+				if(!$userPreferences[$infos['type']][$infos['type'].'_'.$sendType['slug']]) continue;
+				$sendType['callback']($recipient, $infos,$notification);
+			}
 		}
-			
 	}
 
 	public static function clear(){
@@ -106,5 +109,38 @@ class Notification extends Entity{
 			Notification::deleteById($notification->id);
 		}
 	}
+
+	public static function settingPreferences($user, $userPreferences=array()){
+		require_once(__DIR__.SLASH.'UserNotification.class.php');
+
+		// récupération préférences par défault
+		$defaultPreferences = array();
+		foreach(Notification::types() as $slug => $type){
+			$defaultMethods = array();
+			foreach ($type['default_methods'] as $method => $state)
+				$defaultMethods[$slug.'_'.$method] = $state;
+
+			$defaultPreferences[$slug] = $defaultMethods;
+		}
+
+		if(empty($userPreferences)){
+			// récupération préférences utilisateur
+			$preferences = UserPreference::loadAll(array('user'=>$user, 'key'=>'notification_categories'));
+			$userPreferences = array();
+			foreach($preferences as $preference)
+				$userPreferences = json_decode($preference->value, JSON_OBJECT_AS_ARRAY);
+		}
+
+		$notificationPreferences = array();
+		foreach ($defaultPreferences as $slug => $type){
+			$tab = array();
+			$notificationPreferences[$slug] = $defaultPreferences[$slug];
+			if(!isset($userPreferences[$slug])) continue;
+			foreach ($type as $method => $state)
+				$tab[$method] = (isset($userPreferences[$slug][$method])) ? $userPreferences[$slug][$method] : $defaultPreferences[$slug][$method];
+			$notificationPreferences[$slug] = $tab;
+		}
+		return $notificationPreferences;
+	}
 }
 ?>

Some files were not shown because too many files changed in this diff