Browse Source

upgrade core + headless install

idleman 4 years ago
parent
commit
9e749180c9
100 changed files with 8127 additions and 3632 deletions
  1. 7 1
      .htaccess
  2. 15 22
      account.global.php
  3. 1 1
      account.lost.php
  4. 1 1
      account.php
  5. 297 223
      action.php
  6. 36 2
      class/Address.class.php
  7. 2 2
      class/Api.class.php
  8. 36 17
      class/Configuration.class.php
  9. 56 0
      class/Contact.class.php
  10. 27 18
      class/Database.class.php
  11. 7 7
      class/Dictionnary.class.php
  12. 250 102
      class/Entity.class.php
  13. 8 5
      class/Excel.class.php
  14. 19 14
      class/File.class.php
  15. 49 0
      class/History.class.php
  16. 35 25
      class/Image.class.php
  17. 1 1
      class/Log.class.php
  18. 1 1
      class/Mail.class.php
  19. 40 28
      class/Pdf.class.php
  20. 4 4
      class/Plugin.class.php
  21. 44 20
      class/User.class.php
  22. 58 43
      common.php
  23. 41 2
      connector/Mysql.class.php
  24. 145 0
      connector/Oracle.class.php
  25. 135 0
      connector/SqlServer.class.php
  26. 43 5
      connector/Sqlite.class.php
  27. 20 7
      constant-sample.php
  28. 1 1
      css/bootstrap.min.css
  29. 0 0
      css/bootstrap.min.css.map
  30. 732 108
      css/main.css
  31. 143 110
      footer.php
  32. 430 122
      function.php
  33. 71 71
      header.php
  34. 106 0
      img/logo.svg
  35. BIN
      img/logo/default-favicon.png
  36. BIN
      img/logo/default-logo.dark.png
  37. BIN
      img/logo/default-logo.png
  38. 231 112
      install.php
  39. 509 0
      js/filter.component.js
  40. 392 233
      js/main.js
  41. 417 224
      js/plugins.js
  42. 5 0
      js/vendor/bootstrap.min.js
  43. 0 0
      js/vendor/bootstrap.min.js.map
  44. 0 0
      js/vendor/popper.min.js
  45. 1039 0
      js/vendor/trumbowyg.plugins.js
  46. 1 1
      lib/SimpleXLSX/SimpleXLSX.class.php
  47. 162 155
      lib/XLSXWriter/XLSXWriter.class.php
  48. 5 3
      maintenance.php
  49. 68 45
      plugin/activedirectory/ActiveDirectory.class.php
  50. 2 2
      plugin/activedirectory/action.php
  51. 110 67
      plugin/activedirectory/activedirectory.plugin.php
  52. 2 2
      plugin/activedirectory/app.json
  53. 147 128
      plugin/activedirectory/setting.activedirectory.php
  54. 55 0
      plugin/customiser/theme/hackpoint/main.css
  55. 80 25
      plugin/document/Element.class.php
  56. 1 1
      plugin/document/WebDav.class.php
  57. 29 74
      plugin/document/action.php
  58. 2 2
      plugin/document/app.json
  59. 37 17
      plugin/document/css/document.api.css
  60. 37 13
      plugin/document/document.plugin.php
  61. 289 436
      plugin/document/js/document.api.js
  62. 7 5
      plugin/document/js/main.js
  63. 25 0
      plugin/document/tab.client.php
  64. 13 10
      plugin/document/template.document.php
  65. 5 6
      plugin/document/widget.configure.php
  66. 1 3
      plugin/document/widget.php
  67. 7 7
      plugin/example/Contact.class.php
  68. 1 0
      plugin/example/account.example.contact.php
  69. 1 0
      plugin/example/account.example.php
  70. 135 104
      plugin/example/action.php
  71. 2 2
      plugin/example/app.json
  72. 97 52
      plugin/example/example.plugin.php
  73. 43 24
      plugin/example/js/main.js
  74. 17 11
      plugin/example/page.list.php
  75. 1 1
      plugin/example/page.quick.example.php
  76. 52 21
      plugin/example/page.sheet.php
  77. 5 6
      plugin/example/setting.example.php
  78. 97 82
      plugin/export/ExportModel.class.php
  79. 109 105
      plugin/export/action.php
  80. 2 2
      plugin/export/app.json
  81. 16 12
      plugin/export/css/main.css
  82. 2 2
      plugin/export/export.plugin.php
  83. 33 10
      plugin/export/js/component.js
  84. 141 102
      plugin/export/js/main.js
  85. 12 11
      plugin/export/modal.export.model.php
  86. 70 0
      plugin/export/page.documentation.php
  87. 54 127
      plugin/export/page.sheet.php
  88. 16 22
      plugin/export/setting.export.php
  89. 18 5
      plugin/export/template/CsvExport.class.php
  90. 276 165
      plugin/export/template/ExcelExport.class.php
  91. 32 0
      plugin/export/template/FillPdfExport.class.php
  92. 37 13
      plugin/export/template/HtmlExport.class.php
  93. 11 23
      plugin/export/template/PdfExport.class.php
  94. 102 74
      plugin/export/template/TextExport.class.php
  95. 178 113
      plugin/export/template/WordExport.class.php
  96. 19 1
      plugin/hackpoint/css/install.css
  97. 3 2
      plugin/hackpoint/css/main.css
  98. 1 1
      plugin/hackpoint/install.php
  99. 4 4
      plugin/hackpoint/page.sheet.sketch.php
  100. 1 1
      plugin/issue/IssueEvent.class.php

+ 7 - 1
.htaccess

@@ -4,7 +4,13 @@ Options +FollowSymlinks
 RewriteEngine On
 
 
- 
+<Files "schema.json">  
+  Require all denied
+</Files>
+<Files "sql.debug.sql">  
+  Require all denied
+</Files> 
+
 RewriteRule ^api/(.*)$ action.php?action=api&command=$1 [QSA]
 
 

+ 15 - 22
account.global.php

@@ -1,12 +1,14 @@
 <?php 
-global $myUser, $myFirm;
+global $myUser, $myFirm, $conf;
 
-if (!$myUser->connected()) throw new Exception("Vous devez être connecté pour accéder à la page");
+if (!$myUser->connected()) throw new Exception("Vous devez être connecté pour accéder à la page",401);
 
 ?>
 <div id="user-form" class="user-form">
 	<br>
+	<div onclick="account_save()" class="btn btn-success float-right"><i class="fas fa-check"></i> Enregistrer</div>
 	<h3><?php echo $myUser->fullName(); ?> <small class="text-muted">(<?php echo $myUser->login; ?>)</small></h3>
+	<div class="clear"></div>
 	<hr>
 	<div class="row">
 		<div class="col-md-6">
@@ -36,8 +38,8 @@ if (!$myUser->connected()) throw new Exception("Vous devez être connecté pour
 
 	<div class="row">
 		<div class="col-md-6">
-			<label for="login">Identifiant :</label>
-			<input required id="login" name="login" class="form-control" readonly="readonly" placeholder="Identifiant" value="<?php echo $myUser->login; ?>" type="text">
+			<label for="login">Identifiant :</label><small class="text-danger"> <?php echo ' (,'.$conf->get('login_forbidden_char').' interdits)'; ?> </small>
+			<input required id="login" name="login" class="form-control" readonly="readonly" placeholder="Identifiant" value="<?php echo html_decode_utf8($myUser->login); ?>" type="text">
 		</div>
 		<div class="col-md-6">
 			<label for="mail">Mail :</label>
@@ -47,22 +49,22 @@ if (!$myUser->connected()) throw new Exception("Vous devez être connecté pour
 
 	<div class="row">
 		<div class="col-md-6">
-			<label for="password">Mot de passe :</label>
-			<input required id="password" data-show-strength data-generator data-type="password"  name="password" class="form-control" placeholder="Mot de passe" type="password">
+			<label for="password">Mot de passe :</label><small class="text-danger"> <?php echo ($conf->get('password_forbidden_char') == '' ? '' : ' ('.$conf->get('password_forbidden_char').' interdits)'); ?> </small>
+			<input required id="password" data-show-strength data-generator data-type="password"  name="password" class="form-control" placeholder="Mot de passe" type="password" autocomplete="new-password">
 		</div>
 		<div class="col-md-6">
-			<label for="password2">Mot de passe (confirmation) :</label>
-			<input required id="password2" data-type="password" name="password2" class="form-control" placeholder="Mot de passe (confirmation)" type="password">
+			<label for="password2">Mot de passe (confirmation) :</label><small class="text-danger"> <?php echo ($conf->get('password_forbidden_char') == '' ? '' : ' ('.$conf->get('password_forbidden_char').' interdits)'); ?> </small>
+			<input required id="password2" data-type="password" name="password2" class="form-control" placeholder="Mot de passe (confirmation)" type="password" autocomplete="new-password">
 		</div>
 	</div><br>
 	<div class="row">
 		<div class="col-md-3">
 			<label for="firstname">Prénom :</label>
-			<input id="firstname" name="firstname" class="form-control" placeholder="Prénom" type="text" value="<?php echo $myUser->firstname; ?>">
+			<input id="firstname" name="firstname" class="form-control" placeholder="Prénom" type="text" value="<?php echo html_decode_utf8($myUser->firstname()); ?>">
 		</div>
 		<div class="col-md-3">
 			<label for="name">Nom :</label>
-			<input id="name" name="name" class="form-control" placeholder="Nom" type="text" value="<?php echo $myUser->name; ?>">
+			<input id="name" name="name" class="form-control" placeholder="Nom" type="text" value="<?php echo html_decode_utf8($myUser->lastname()); ?>">
 		</div>
 		<div class="col-md-6">
 			<label for="function">Fonction :</label>
@@ -82,21 +84,12 @@ if (!$myUser->connected()) throw new Exception("Vous devez être connecté pour
 	<div class="row mb-2">
 		<div class="col-md-6">
 			<label for="">Compte créé le :</label>
-			<input class="form-control-plaintext" readonly="readonly" type="text" value="<?php echo date('d/m/Y H:i', $myUser->created); ?>">
+			<input class="form-control-plaintext" readonly="readonly" type="text" value="<?php echo empty($myUser->created) ? '-' : date('d/m/Y H:i', $myUser->created); ?>">
 		</div>
 		<div class="col-md-6">
 			<label for="manager">Manager :</label>
 			<input id="manager" name="manager" readonly="readonly" class="form-control-plaintext" placeholder="Nom de manager" type="text" value="<?php echo is_object($myUser->manager) ? $myUser->manager->fullName():' - '; ?>">
 		</div>
-	</div>
-
+	</div><br>
 	<?php Plugin::callHook('account_global'); ?>
-
-	<hr/>
-	<div class="row mt-2 mb-2">
-		<div class="col-md-12">
-			<div onclick="account_save()" class="btn btn-success right"><i class="fas fa-check"></i> Enregistrer</div>
-		</div>
-	</div>	
-	
-</div>
+</div>

+ 1 - 1
account.lost.php

@@ -60,7 +60,7 @@ $page = basename($_SERVER['PHP_SELF']);
 			<p>Un e-mail sera envoyé à la boite spécifiée si celle ci est liée à un compte.</p>
 			<label>Adresse e-mail du compte</label>
 			<input class="form-control col-sm-3 m-auto text-center" placeholder="email@email.com" type="text" id="mail">
-			<div class="btn btn-info col-sm-3 mt-2" onclick="account_lost_password()"><i class="far fa-envelope-open"></i> Envoyer</div>
+			<div class="btn btn-primary col-sm-3 mt-2" id="lost-password-send" onclick="account_lost_password(this);"><i class="far fa-envelope-open"></i> Envoyer</div>
 		<?php endif; ?>
 	</div>
 </div>

+ 1 - 1
account.php

@@ -16,7 +16,7 @@ $page = basename($_SERVER['PHP_SELF']);
 <div class="row">
 	<div class="col-md-3">
 		<div class="list-group">
-			<h5 class="list-group-item">Préférences</h5>
+			<span class="list-group-item"><h5 class="mb-0">Préférences</h5></span>
 			<?php foreach($accountMenu as $item): ?>
 				<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; ?>

File diff suppressed because it is too large
+ 297 - 223
action.php


+ 36 - 2
class/Address.class.php

@@ -6,10 +6,12 @@
  * @license copyright
  */
 class Address extends Entity{
-	public $id,$label,$type,$street,$complement,$city,$zip,$country,$state;
+	public $id,$entity_id,$entity,$label,$type,$street,$complement,$city,$zip,$country,$state;
 	public $fields =
 	array(
 		'id' => 'key',
+		'entity_id' => 'int',
+		'entity' => 'string',
 		'label' => 'string',
 		'type' => 'string',
 		'street' => 'string',
@@ -19,5 +21,37 @@ class Address extends Entity{
 		'country' => 'string',
 		'state' => 'string'
 	);
+
+	public function mapUrl(){
+		$encoded = urlencode($this->street).', '.urlencode($this->zip).' '.urlencode($this->city);
+		return 'https://www.google.com/maps/place/'.$encoded;
+	}
+
+	//retourne l'adresse sur une seule ligne au format street, complement, zip city country
+	public function fullName(){
+		if($this->id==0) return '';
+		$fullname = '';
+		if(!empty($this->street)) $fullname .= $this->street;
+		if(!empty($this->complement)) $fullname .= ', '.$this->complement;
+		if($fullname!='') $fullname .= ', ';
+		if(!empty($this->zip)) $fullname .= $this->zip;
+		if(!empty($this->city)) $fullname .= ' '.$this->city;
+		if(!empty($this->country)) $fullname .= ' '.$this->country;
+		return $fullname;
+	}
+
+	//remplis un composant location à partir des infos de la classe courante
+	public function toDataAttributes($plainText = true){
+		$array = array();
+		$plain = '';
+		foreach (array('street','complement','city','zip','country') as $key) {
+			$array[$key] = $this->$key;
+			$plain = ' data-'.$key.' = "'.(empty($this->$key) ? '' : $this->$key).'"';
+		}
+		return !$plainText ? $array : $plain;
+		
+
+
+	}
 }
-?>
+?>

+ 2 - 2
class/Api.class.php

@@ -75,8 +75,8 @@ class Api{
 			);
 			$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);
+			//format de l'url : /api/v{version}/
+			if(!isset($request['version']) || !is_numeric(substr($request['version'],1)) || substr($request['version'], 0,1)!='v') 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) {

+ 36 - 17
class/Configuration.class.php

@@ -55,17 +55,16 @@ class Configuration extends Entity
     //Met en page (tableau html) un set de configuration générique
     public static function html($name){
         global $conf;
-        $options = $GLOBALS['setting'][$name]; ?>
+        $options = isset($GLOBALS['setting'][$name]) ? $GLOBALS['setting'][$name] : array(); ?>
 
-        <table id="<?php echo $name; ?>-setting-form" class="table table-striped">
+        <table id="<?php echo $name; ?>-setting-form" class="table table-striped <?php echo $name; ?>-setting-form">
             <tbody>
-                <?php foreach($options as $key=>$infos): ?>
-                    <?php
-                        $input = '';
-                     if(!is_array($infos)): ?>
+                <?php foreach($options as $key=>$infos):
+                    $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>
+                    <tr class="<?php echo $key; ?>">
                         <th class="align-middle"><?php echo $infos['label']; ?>
                             <?php if (isset($infos['legend'])): ?>
                             <small class="text-muted"> - <?php echo $infos['legend']; ?></small>
@@ -74,10 +73,11 @@ class Configuration extends Entity
                         
                         <td class="align-middle position-relative">
                             <?php $attributes = array();
-                            if(isset($infos['placeholder'])) $attributes[] = 'placeholder="'.$infos['placeholder'].'"';
+                            if(isset($infos['placeholder'])) $attributes['placeholder'] = 'placeholder="'.$infos['placeholder'].'"';
                             $attributes['class'] = 'class="form-control"';
-                            $attributes[] = 'id="'.$key.'"';
-                            $attributes['value'] = 'value="'.htmlentities($conf->get($key)).'"';
+                            $attributes['id'] = 'id="'.$key.'"';
+                            $confValue = $conf->get($key);
+                            $attributes['value'] = 'value="'.(isset($infos['default']) && empty($confValue) ? htmlentities($infos['default']) : htmlentities($confValue)).'"';
 
                             if(isset($infos['parameters'])){
                                 foreach($infos['parameters'] as $attribute=>$parameter){
@@ -89,7 +89,7 @@ class Configuration extends Entity
                             switch($infos['type']){
                                 case 'password':
                                     $attributes['type'] = 'data-type="password"';
-                                    $input = '<input type="text" '.implode(' ',$attributes).' >'; 
+                                    $input = '<input type="text" autocomplete="new-password" '.implode(' ',$attributes).' >'; 
                                 break;
                                 case 'dictionnary':
                                     $attributes['type'] = 'data-type="dictionnary"';
@@ -98,17 +98,20 @@ class Configuration extends Entity
                                 break;
                                 case 'checkbox':
                                     unset($attributes['class']);
-                                    unset($attributes['value']);
                                     $attributes['type'] = 'data-type="checkbox"';
-                                    if($conf->get($key)) $attributes['value'] = 'checked="checked"';
+
+                                    if(isset($infos['default']) && $confValue == '')
+                                        $attributes['value'] = $infos['default'] ? 'checked="checked"' : '';
+                                    else
+                                        $attributes['value'] = $confValue ? 'checked="checked"' : '';
+                                    
                                     $input = '<label class="mb-0 pointer no-select"><input type="checkbox" '.implode(' ',$attributes).' > Activer</label>'; 
                                 break;
                                 case 'select':
                                     unset($attributes['value']);
-
-                                    $input = '<select '.implode(' ',$attributes).'>'; 
+                                    $input = '<select '.implode(' ',$attributes).'>';
                                     foreach($infos['values'] as $id=>$label)
-                                        $input .= '<option value="'.$id.'" '.($id==$conf->get($key)?'selected="selected"':'').' type="checkbox">'.$label.'</option>'; 
+                                        $input .= '<option value="'.$id.'" '.($id==$confValue || (isset($infos['default']) && $infos['default'] == $id && $confValue == '')?'selected="selected"':'').'>'.$label.'</option>';
                                     $input .= '</select>'; 
                                 break;
                                case 'user':
@@ -131,9 +134,25 @@ class Configuration extends Entity
                                 case 'textarea':
                                     $input = '<textarea '.implode(' ',$attributes).' >'.htmlentities($conf->get($key)).'</textarea>';
                                 break;
+                                case 'wysiwyg':
+                                    $attributes['data-type'] = 'data-type="wysiwyg"';
+                                    $attributes['class'] = 'class="m-0"';
+                                    unset($attributes['value']);
+                                    $input = '<textarea '.implode(' ',$attributes).' >'.htmlentities($conf->get($key)).'</textarea>';
+                                break;
                                 case 'file':
+                                    //Si input file "multiple", possibilité de normlaiser le
+                                    //tableau $_FILES récupéré avec la fonction => normalize_php_files();
+                                    unset($attributes['class']);
                                     $input = '<input type="file" '.implode(' ',$attributes).' >';
                                 break;
+                                case 'dropzone':
+                                    $attributes['id'] = 'id="document"';
+                                    $documents = preg_replace("/documents=\"(.*)\"/i", "$1", $attributes['documents']);
+                                    unset($attributes['documents']);
+
+                                    $input = '<div data-type="dropzone" '.(isset($attributes['data-label']) ? $attributes['data-label'] : 'Faites glisser vos documents').' '.(isset($attributes['data-delete']) ? $attributes['data-delete'] : '').' '.(isset($attributes['data-allowed']) ? $attributes['data-allowed'] : '').' class="form-control" '.implode(' ',$attributes).'>'.$documents.'</div>';
+                                break;
                                 default:
                                     Plugin::callHook('configuration_fields',array(&$input, $infos, $attributes));
                                     if(empty($input)) $input = '<input type="text" '.implode(' ',$attributes).' >';
@@ -162,7 +181,7 @@ class Configuration extends Entity
      */
     public function get($key)
     {
-        return isset($this->confTab[$key]) ? $this->confTab[$key] : '';
+        return isset($this->confTab[$key]) ? htmlspecialchars_decode($this->confTab[$key]) : '';
     }
 
     /**

+ 56 - 0
class/Contact.class.php

@@ -0,0 +1,56 @@
+<?php
+/**
+ * Define a address
+ * @author  Valentin MORREEL
+ * @category Core
+ * @license copyright
+ */
+class Contact extends Entity{
+	public $id,$entity,$entity_id,$label,$type,$value,$state;
+
+	//Phone constants
+	const PHONE = 'phone';
+	const MOBILE= 'mobile';
+	//Mail constants
+	const PERSONAL_MAIL = 'personal_mail';
+	const PROFESSIONAL_MAIL = 'professional_mail';
+
+	public $fields = array(
+		'id' => 'key',
+		'entity' => 'string',
+		'entity_id' => 'int',
+		'label' => 'string',
+		'type' => 'string',
+		'value' => 'string',
+		'state' => 'string'
+	);
+
+	public static function types($key=null){
+		$types = array(
+			self::PHONE => array(
+				'label' => "Tél. Fixe",
+				'icon' => "",
+				'color' => "",
+			),
+			self::MOBILE => array(
+				'label' => "Tél. Mobile",
+				'icon' => "",
+				'color' => "",
+			),
+			self::PERSONAL_MAIL => array(
+				'label' => "Mail perso.",
+				'icon' => "",
+				'color' => "",
+			),
+			self::PROFESSIONAL_MAIL => array(
+				'label' => "Mail pro.",
+				'icon' => "",
+				'color' => "",
+			),
+		);
+		Plugin::callHook('contact_types', array(&$types));
+		
+		return isset($key) && isset($types[$key]) ? $types[$key] : $types;
+	}
+}
+?>

+ 27 - 18
class/Database.class.php

@@ -11,7 +11,7 @@ require_once(dirname(__DIR__).DIRECTORY_SEPARATOR.'constant.php');
  */
 class Database
 {
-    public $connection = null;
+    public $connections = array();
     public static $instance = null;
     private function __construct()
     {
@@ -25,40 +25,49 @@ class Database
      * @param <Aucun>
      * @return <pdo> $instance
      */
-    public static function instance()
+    public static function instance($baseuid)
     {
         if (self::$instance === null) {
             self::$instance = new self();
         }
 
-        return self::$instance->connection;
+        if(!isset(self::$instance->connections[$baseuid]))
+            self::$instance->connect($baseuid);
+
+       
+        return self::$instance->connections[$baseuid];
     }
 
-    public static function version(){
+    public static function version($baseUid = 'local'){
          try {
             $db = new self();
             $db->connect();
-            return $db->connection->getAttribute(PDO::ATTR_SERVER_VERSION);
+            return $db->connections[$baseUid]->getAttribute(PDO::ATTR_SERVER_VERSION);
          } catch (Exception $e) {
             return 'Connection à la base impossible : '. $e->getMessage();
         }
     }
 
-    public function connect()
+    public function connect($baseUid = 'local')
     {
         try {
-         $base = BASE_SGBD;
-         require_once(__ROOT__.'connector/'.$base.'.class.php');
-         $connectionString = str_replace(
-             array('{{ROOT}}','{{BASE_HOST}}','{{BASE_NAME}}','{{BASE_LOGIN}}','{{BASE_PASSWORD}}'),
-             array(__ROOT__,BASE_HOST,BASE_NAME,BASE_LOGIN,BASE_PASSWORD),
-             $base::connection);
-         $this->connection = new PDO($connectionString, BASE_LOGIN, BASE_PASSWORD,array(
-    PDO::ATTR_PERSISTENT => true
-));
-         $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
-         if(method_exists ( $base , 'beforeTransaction' ))
-         $base::beforeTransaction($this->connection);
+         global $databases_credentials;
+         $database = $databases_credentials[$baseUid];
+         $connector = $database['connector'];
+         require_once(__ROOT__.'connector/'.$connector.'.class.php');
+
+         $connectionString = $connector::connection;
+
+         foreach ($database as $key => $value) {
+             $connectionString = str_replace('{{'.$key.'}}',$value,$connectionString);
+         }
+
+         $connectionString = str_replace('{{ROOT}}',__ROOT__,$connectionString);
+        
+         $this->connections[$baseUid] = new PDO($connectionString, $database['login'], $database['password'],$connector::pdo_attributes());
+     
+         if(method_exists ( $connector , 'beforeTransaction' ))
+         $connector::beforeTransaction($this->connections[$baseUid]);
       
      } catch (Exception $e) {
         echo 'Connection à la base impossible : ', $e->getMessage();

+ 7 - 7
class/Dictionnary.class.php

@@ -28,9 +28,9 @@ class Dictionnary extends Entity
 
 	//Retourne une liste formatée en fonction de son slug
 	//(retourne ses enfants si le parametre childs est à true)
-	public static function slugToArray($slug, $childs = false){
+	public static function slugToArray($slug, $childs = false, $state = self::ACTIVE){
 		$listArray = array();
-		$lists = self::bySlug($slug, $childs);
+		$lists = self::bySlug($slug, $childs, $state);
 		if(!$childs) return $lists;
 
 		foreach($lists as $list)
@@ -39,17 +39,17 @@ class Dictionnary extends Entity
 	}
 
 	//Retourne une liste en fonction de son slug (retourne ses enfants si le parametre childs est à true)
-	public static function bySlug($slug, $childs = false){
-		if($childs) return self::childs(array('slug'=>$slug));
-		return self::load(array("slug"=>$slug));
+	public static function bySlug($slug, $childs = false, $state = self::ACTIVE){
+		if($childs) return self::childs(array('slug'=>$slug,'state:IN'=>$state));
+		return self::load(array("slug"=>$slug,'state:IN'=>$state));
 	}
 
 	public static function childs($param = array()) {
 		$childs = array();
 		$parent = self::load($param);
 		if(!$parent) return $childs;
-
-		foreach (self::loadAll(array('parent'=>$parent->id, 'state'=>self::ACTIVE), array( ' label ASC ')) as $child) {
+		$state = isset($param['state:IN']) ? $param['state:IN'] : self::ACTIVE;
+		foreach (self::loadAll(array('parent'=>$parent->id, 'state:IN'=>$state), array( ' label ASC ')) as $child) {
 			$childs[] = $child;
 		}
 		return $childs;

+ 250 - 102
class/Entity.class.php

@@ -2,14 +2,20 @@
 
 require_once __DIR__.'/../constant.php';
 require_once(__ROOT__.'class'.SLASH.'Database.class.php');
+require_once(__ROOT__.'connector'.SLASH.'Oracle.class.php');
+require_once(__ROOT__.'connector'.SLASH.'Mysql.class.php');
+require_once(__ROOT__.'connector'.SLASH.'Sqlite.class.php');
+require_once(__ROOT__.'connector'.SLASH.'SqlServer.class.php');
 
 /**
  @version 2
  **/
  class Entity
  {
-    public $debug = false,$pdo = null;
+    public $debug = false,$pdo = null,$baseUid= 'local';
     public $created,$updated,$creator,$updater,$joins;
+    public $foreignColumns = array();
+    public $fieldMapping = array();
 
     public static $lastError = '';
     public static $lastResult = '';
@@ -23,6 +29,7 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         if (!isset($this->TABLE_NAME)) {
             $this->TABLE_NAME = strtolower(get_called_class());
         }
+
         $this->connect();
         $this->fields['created'] = 'datetime';
         $this->fields['updated'] = 'datetime';
@@ -30,15 +37,25 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         $this->fields['creator'] = 'string';
         $this->joins = array();
         $this->created = time();
+
+       
+         foreach ($this->fields as $field => $type) {
+            if(!is_array($type)) $type = array('type'=>$type,'column'=>$field);
+            if(!isset( $type['column'] )) $type['column'] = $field;
+            $this->fieldMapping[$field] = $type;
+        }
+
+
         global $myUser;
         if(is_object($myUser) && $myUser->login!='') $this->creator = $myUser->login;
-        
     }
 
     public function connect()
     {
-        $this->pdo = Database::instance();
-        
+        $this->pdo = Database::instance($this->baseUid);
+        global $databases_credentials;
+
+        $this->connector = $databases_credentials[$this->baseUid]['connector'];
     }
 
     public function __toString()
@@ -64,12 +81,12 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
     }
 
     //Comparaison de deux instances d'une même entité, retourne les champs ayant changés uniquement
-    public static function compare($obj1,$obj2){
+    public static function compare($obj1,$obj2,$ignore=array()){
         $class = get_called_class();
         $instance = new $class();
         $compare = array();
         foreach ($obj1->fields as $field => $type) {
-            if($field == 'updated' || $field == 'updater') continue;
+            if($field == 'updated' || $field == 'updater' || in_array($field, $ignore)) continue;
   
             if($obj1->$field != $obj2->$field){
                 if($type=='int' && (($obj1->$field==0 && $obj2->$field =='') || ($obj2->$field=='' && $obj1->$field ==0)) ) continue;
@@ -102,11 +119,7 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         }
     }
 
-    public function sgbdType($type) {
-        $sgbd = BASE_SGBD;
-        $types = $sgbd::types(); 
-        return isset($types[$type]) ? $types[$type] : $types['default'];
-    }
+ 
 
     public function closeDatabase() {
         // $this->close();
@@ -128,12 +141,12 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         return $i==''?$slug:$slug.'-'.$i;
     }
 
-    public static function tableName()
+    public static function tableName($escapeName = false)
     {
         $class = get_called_class();
         $instance = new $class();
-    
-        return ENTITY_PREFIX.$instance->TABLE_NAME;
+        $connector = $instance->connector;
+        return $escapeName ? $connector::table_escape.ENTITY_PREFIX.$instance->TABLE_NAME.$connector::table_escape : ENTITY_PREFIX.$instance->TABLE_NAME;
     }
 
     // GESTION SQL
@@ -203,10 +216,11 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
     {
         $class = get_called_class();
         $instance = new $class();
-        $sgbd = BASE_SGBD;
-        $sql = $sgbd::truncate();
+        $connector = $instance->connector;
+        $sql = $connector::truncate();
         $query = Entity::render($sql,array(
-           'table' => $instance->tableName()
+           'table' => $instance->tableName(),
+           'fieldMapping' => $instance->fieldMapping
            ));
         
         $instance->customExecute($query);
@@ -226,15 +240,20 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         $class = get_called_class();
         $instance = new $class();
         $fields = array();
+
+        $connector = $instance->connector;
+        $types = $connector::types(); 
+       
         
         foreach ($instance->fields as $field => $type) 
-            $fields[$field] =  $instance->sgbdType($type);
+            $fields[$field] =  isset($types[$type]) ? $types[$type] : $types['default'];
+        
         
-        $sgbd = BASE_SGBD;
-        $sql = $sgbd::create();
+        $sql = $connector::create();
         $query = Entity::render($sql,array(
            'table' => $instance->tableName(),
-           'fields' => $fields
+           'fields' => $fields,
+           'fieldMapping' => $instance->fieldMapping
            ));
 
         $instance->customExecute($query);
@@ -245,10 +264,11 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
     {
         $class = get_called_class();
         $instance = new $class();
-        $sgbd = BASE_SGBD;
-        $sql = $sgbd::drop();
+        $connector = $instance->connector;
+        $sql = $connector::drop();
         $query = Entity::render($sql,array(
-           'table' => $instance->tableName()
+           'table' => $instance->tableName(),
+           'fieldMapping' => $instance->fieldMapping
            ));
         
         $instance->customExecute($query);
@@ -277,6 +297,8 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         $numericType = array('object', 'timestamp', 'datetime', 'date', 'int', 'float', 'decimal');
         $stringType = array('string', 'longstring', 'default');
 
+        $connector = $this->connector;
+
         if (isset($this->id) && $this->id > 0) {
          
             $fields = array();
@@ -291,12 +313,13 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
                 $i++;
             }
             $data[':id'] = $this->id;
-            $sgbd = BASE_SGBD;
-            $sql = $sgbd::update();
+            
+            $sql = $connector::update();
 
             $query = self::render($sql,array(
                'table' => $this->tableName(),
                'fields' => $fields,
+               'fieldMapping' => $this->fieldMapping,
                'filters' => array('id'=>':id'),
                ));
         } else {
@@ -313,12 +336,13 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
                 $i++;
             }
             
-            $sgbd = BASE_SGBD;
-            $sql = $sgbd::insert();
+           
+            $sql = $connector::insert();
 
             $query = self::render($sql,array(
                'table' => $this->tableName(),
-               'fields' => $fields
+               'fields' => $fields,
+               'fieldMapping' => $this->fieldMapping
             ));
         }
         $this->customExecute($query, $data);
@@ -338,8 +362,7 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
      *
      * @return Aucun retour
      */
-    public static function change($columns, $columns2 = array(), $operation = '=')
-    {
+    public static function change($columns, $columns2 = array(), $operation = '=') {
         $class = get_called_class();
         $instance = new $class();
         
@@ -354,18 +377,33 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         $filters = array();
         $i = 0;
         foreach ($columns2 as $field => $value) {
-            $data[':_'.$i] = $value;
-            $filters[$field] = ':_'.$i;
-            $i++;
+            //Gestion du IN
+            if(strlen($field)>=3 && substr($field,-3) == ':IN'){
+                $filters[$field] = array();
+                foreach (explode(',',$value) as $v2) {
+                    $tag = ':_'.$i;
+                    $filters[$field][] = $tag;
+                    $data[$tag] = $v2;
+                    $i++;
+                }
+                $filters[$field] = implode(',',$filters[$field]);
+             //Gestion des opérateurs classiques
+            } else {
+                $tag = ':_'.$i;
+                $filters[$field] = $tag;
+                $data[$tag] = $value;
+                $i++;
+            }
         }
-
-        $sgbd = BASE_SGBD;
-        $sql = $sgbd::update();
+        $connector = $instance->connector;
+        $sql = $connector::update();
         $query = Entity::render($sql,array(
             'table' => $instance->tableName(),
             'fields' => $fields,
             'filters' => $filters,
+            'fieldMapping' => $instance->fieldMapping
         ));
+
         $instance->customExecute($query, $data);
     }
 
@@ -403,40 +441,51 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
      * @return <Array<Entity>> $Entity
      */
    
-    public static function loadAll($columns = array(), $order = null, $limit = null, $selColumn = array('*'),$joins = 0)
+    public static function loadAll($columns = array(), $order = null, $limit = null, $selColumn = array('*'), $joins = 0)
     {
+        $class = get_called_class();
+        $instance = new $class();
+        $connector = $instance->connector;
+
         $values = array();
         $i=0;
+        $filters = array();
         foreach($columns as $key=>$value){
-            //Gestion du IN
-            if(strlen($key)>=3 && substr($key,-3) == ':IN'){
-                $columns[$key] = array();
-                foreach (explode(',',$value) as $v2) {
-                    $tag = ':'.$i;
-                    $columns[$key][]= $tag;
-                    $values[$tag] = $v2;
-                    $i++;
-                }
-                $columns[$key] = implode(',',$columns[$key]);
-             //Gestion des opérateurs classiques
-            } else {
-                $tag = ':'.$i;
-                $columns[$key] = $tag;
-                $values[$tag] = $value;
-                $i++;
+            $filter = array(
+                'operator' => '=',
+                'field' => $key,
+                'postoperator' => ''
+            );
+            if(strpos($key,':')!==false){
+                $infos = explode(':',$key);
+                $filter['operator'] = $infos[1];
+                $filter['field'] = $infos[0];
             }
+
+            $fieldInfos = $instance->fieldMapping[$filter['field']];
+            $filter['type'] = $fieldInfos['type'];
+            $filter['column'] = $fieldInfos['column'];
+
+            $connector::processField($filter,$value,$values,$i);
+            $filters[] = $filter;
+         }
+         
+         if(!empty($order)){
+             foreach ($order as $key=>$clause) {
+                foreach ($instance->fieldMapping as $attribute => $infos) {
+                    $order[$key] = str_replace( $attribute,$infos['column'],$order[$key]);
+                }
+             }
          }
          
-         $class = get_called_class();
-        
-         $instance = new $class();
          $data = array(
             'table' => $instance->tableName(),
             'selected' => $selColumn,
             'limit' =>  !isset($limit) || count($limit) == 0 ? null: $limit,
             'orderby'  => !isset($order) || count($order) == 0 ? null: $order,
-            'filter' => !isset($columns) ||  count($columns) == 0 ? null: $columns
-            );
+            'filter' => !isset($filters) ||  count($filters) == 0 ? null: $filters,
+            'fieldMapping' => $instance->fieldMapping
+        );
         $data['joins']  = array();
         if($joins!=0){
             foreach ($data['selected'] as $k=>$column) {
@@ -444,11 +493,12 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
             }
             $data = self::recursiveJoining($instance,$data,$joins);
         }
-        $sgbd = BASE_SGBD;
-        $sql = $sgbd::select();
-        $sql = Entity::render($sql,$data);
         
-        return $instance->customQuery($sql, $values, true,$joins);
+            
+        $sql = $connector::select();
+        $sql = Entity::render($sql,$data);
+
+        return $instance->customQuery($sql, $values, true, $joins);
     }
 
     /**
@@ -503,25 +553,43 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
      * @category manipulation SQL
      * @return<Integer> nombre de ligne dans l'entité'
      */
-    public static function rowCount($columns = null) {
+    public static function rowCount($columns = array()) {
         $values = array();
-              
+        $class = get_called_class();
+        $instance = new $class();
+        $connector = $instance->connector;
+
         $i=0;
+        $values = array();
+        $filters = array();
         foreach($columns as $key=>$value){
-            $tag = ':'.$i;
-            $columns[$key] = $tag;
-            $values[$tag] = $value;
-            $i++;
+            $filter = array(
+                'operator' => '=',
+                'field' => $key,
+                'postoperator' => ''
+            );
+            if(strpos($key,':')!==false){
+                $infos = explode(':',$key);
+                $filter['operator'] = $infos[1];
+                $filter['field'] = $infos[0];
+            }
+
+            $fieldInfos = $instance->fieldMapping[$filter['field']];
+            $filter['type'] = $fieldInfos['type'];
+            $filter['column'] = $fieldInfos['column'];
+
+            $connector::processField($filter,$value,$values,$i);
+            $filters[] = $filter;
         }
-        $class = get_called_class();
-        $instance = new $class();
+
         $data = array(
             'table' => $class::tableName(),
             'selected' => 'id' ,
-            'filter' =>  count($columns) == 0 ? null: $columns
+            'filter' =>  count($filters) == 0 ? null: $filters,
+            'fieldMapping' => $instance->fieldMapping
         );
-        $sgbd = BASE_SGBD;
-        $sql = $sgbd::count();
+        
+        $sql = $connector::count();
         $execQuery = $instance->customQuery(Entity::render($sql,$data), $values);
         $row = $execQuery->fetch();
 
@@ -598,8 +666,32 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
                         }
                         $sql.= $occurence;
                     }
-                } else {
+                }else if($tag =='filter'){
                     //filters
+
+                    foreach($values as $key=>$value){
+                        $i++;
+
+                        $last = $i == count($values);
+                        $operator = $value['operator'];
+                        $postoperator = $value['postoperator'];
+                        $key = $value['column'];
+                        
+                        
+                        
+                        $occurence = str_replace(array('{{key}}','{{value}}','{{operator}}','{{postoperator}}'),array($key,
+                            $value['tag'],
+                            $operator,
+                            $postoperator),
+                        $sqlTpl); 
+                        $occurence = preg_replace_callback('/{{\;}}(.*?){{\/\;}}/',function($matches) use ($last){
+                            return $last? '': $matches[1];
+                        },$occurence);
+
+                        $sql.= $occurence;
+                    }
+                } else {
+                    //Autre boucles
                     foreach($values as $key=>$value){
                         $i++;
                         $last = $i == count($values);
@@ -654,7 +746,7 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
      * @category manipulation SQL
      * @return<boolean> existe (true) ou non (false)
      */
-    public static function exist($columns = null) {
+    public static function exist($columns = array()) {
         $result = self::rowCount($columns);
         return $result != 0;
     }
@@ -678,24 +770,42 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
      */
     public static function delete($columns, $limit = array()) {
         $values = array();
-                  
+        $class = get_called_class();
+        $instance = new $class();
+        $connector = $instance->connector;
+        
         $i=0;
+        $values = array();
+        $filters = array();
         foreach($columns as $key=>$value){
-            $tag = ':'.$i;
-            $columns[$key] = $tag;
-            $values[$tag] = $value;
-            $i++;
+            $filter = array(
+                'operator' => '=',
+                'field' => $key,
+                'postoperator' => ''
+            );
+            if(strpos($key,':')!==false){
+                $infos = explode(':',$key);
+                $filter['operator'] = $infos[1];
+                $filter['field'] = $infos[0];
+            }
+
+            $fieldInfos = $instance->fieldMapping[$filter['field']];
+            $filter['type'] = $fieldInfos['type'];
+            $filter['column'] = $fieldInfos['column'];
+
+            $connector::processField($filter,$value,$values,$i);
+            $filters[] = $filter;
         }
 
-        $class = get_called_class();
-        $instance = new $class();
+        
         $data = array(
            'table' => $class::tableName(),
            'limit' =>  count($limit) == 0 ? null: $limit,
-           'filter' =>  count($columns) == 0 ? null: $columns
+           'filter' =>  count($filters) == 0 ? null: $filters,
+           'fieldMapping' => $instance->fieldMapping
            );
-        $sgbd = BASE_SGBD;
-        $sql = $sgbd::delete();
+        
+        $sql = $connector::delete();
 
         return $instance->customExecute(Entity::render($sql,$data), $values);
     }
@@ -717,29 +827,33 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         if(!is_array($columns)) $columns = array($columns);
         $columns = array_filter($columns);
 
-        $sgbd = BASE_SGBD;
+        $class = get_called_class();
+        $instance = new $class();
+        $connector = $instance->connector;
         
         $class = get_called_class();
+        $instance = new $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),
+                'fieldMapping' => $instance->fieldMapping
             );
 
-            $results = $class::staticQuery(Entity::render($sgbd::count_index(),$data));
+            $results = $class::staticQuery(Entity::render($connector::count_index(),$data));
             $exists = $results->fetch();
             
             if($mode){
-                if($exists['exists'] != 1) $class::staticQuery(Entity::render($sgbd::create_index(),$data));
+                if($exists['exists'] != 1) $class::staticQuery(Entity::render($connector::create_index(),$data));
             }else{
-                if($exists['exists'] > 0) $class::staticQuery(Entity::render($sgbd::drop_index(),$data));
+                if($exists['exists'] > 0) $class::staticQuery(Entity::render($connector::drop_index(),$data));
             }
         }
     }
      
-    public static function paginate($itemPerPage,$currentPage,&$query,$data){
+    public static function paginate($itemPerPage,$currentPage,&$query,$data,$alias=''){
         global $_;
         $class = get_called_class();
         $obj = new $class();
@@ -748,15 +862,14 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
 
         $queryNumber = $query;
 
-         
-        $queryNumber = preg_replace("/(SELECT.+[\n|\t]*FROM[\s\t\r\n])({{table}}|`?".$obj->tableName()."`?)/iU", 'SELECT DISTINCT '.$obj->tableName().'.'.$key.' FROM $2',$queryNumber);
-       
+        $queryNumber = preg_replace("/(?<!\([^(\)])(SELECT.+[\n|\t]*FROM[\s\t\r\n])({{table}}|`?".$obj->tableName()."`?)(?![^(\)]*\))/iU", 'SELECT DISTINCT '.(!empty($alias) ? $alias : $obj->tableName()).'.'.$key.' FROM $2',$queryNumber);
+
         $queryNumber = $class::staticQuery('SELECT COUNT(*) FROM ('.$queryNumber.') number',$data)->fetch();
-       
+
         $number = $queryNumber[0];
         $pageNumber = $number / $itemPerPage;
         if($currentPage > $pageNumber) $currentPage = 0;
-      
+        
         $limit = ' LIMIT '.($currentPage*$itemPerPage).','.$itemPerPage;
         $query .= $limit;
         return array(
@@ -786,23 +899,34 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
     public static function provide($parameter = 'id',$join=0){
         global $_;
         $class = get_called_class();
-        return !empty($_[$parameter]) ? $class::getById($_[$parameter],$join) : new $class() ;
+        return !empty($_[$parameter]) ? $class::getById($_[$parameter],$join) : new $class();
     }
 
+    // L'alias utilisé pour la colonne du join doit avoir la syntaxe [table]_join_[field]
+    // Ex : address_join_street
     public static function staticQuery($query, $data = array(), $fill = false,$joins = 0) {
         $class = get_called_class();
         $instance = new $class();
+
         return $instance->customQuery($query, $data, $fill,$joins);
     }
 
     public function customQuery($query, $data = array(), $fill = false,$joins = 0) {
-        $query = str_replace('{{table}}', '`'.$this->tableName().'`', $query);
+        $query = str_replace('{{table}}', $this->tableName(true), $query);
+
+        $mapping = $this->fieldMapping;
+        $query = preg_replace_callback('/{{([^}]*)}}/si', function($match) use ($mapping){
+          
+            return isset($mapping[$match[1]]) && isset($mapping[$match[1]]['column']) ? $mapping[$match[1]]['column'] : $match[0];
+        }, $query);
+
         self::$lastQuery = $query;
         
         try{
             if(BASE_DEBUG) self::logFile($query.' :: '.json_encode($data, JSON_UNESCAPED_UNICODE));
             $results = $this->pdo->prepare($query);
             $results->execute($data);
+    
             if (!$results) throw new Exception(json_encode($this->pdo->errorInfo()));
         }catch(Exception $e){
             self::$lastError = $e->getMessage();
@@ -816,23 +940,47 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
         
         $class = get_class($this);
         $objects = array();
-        $results = $results->fetchAll();
+        $results = $results->fetchAll(PDO::FETCH_ASSOC);
+
         self::$lastResult = $results;
+
         foreach ($results as $queryReturn) {
+
             $object = new $class();
             foreach ($this->fields as $field => $type) {
-                if (isset($queryReturn[$field])) {
-                    $object->{$field} = $queryReturn[$field];
+                $dbField = $field;
+                if(is_array($type)){
+
+                    if(isset($type['column'])) $dbField = $type['column'];
+                    $type = $type['type'];
+
+                }
+
+                //if($this->tableName()=='WORKORDER') var_dump($queryReturn);
+                if (isset($queryReturn[$dbField])) {
+                    $object->{$field} = $queryReturn[$dbField];
+                    unset($queryReturn[$dbField]);
                 }
-            }            
+            }   
+                   
             if($joins>0) $object = self::recursiveJoiningFill($object,$queryReturn,$joins);
             
+            foreach ($queryReturn as $key => $value) {
+                if(!is_numeric($key)) $object->foreignColumns[$key] = $value;
+            }  
+            
             $objects[] = $object;
             unset($object);
         }
+
         return $objects == null ? array()  : $objects;
     }
 
+    //Récuperation d'une/plusieurs colonne non référencée dans l'objet mais récuperée dans une requete static query
+    public function foreign($key=null){
+        if(!isset($key))  return $this->foreignColumns;
+        return isset($this->foreignColumns[$key]) ? $this->foreignColumns[$key]  : '';
+    }
 
     private static function recursiveJoiningFill($object,$queryReturn,$iterations){
         if($iterations == 0) return $object;

+ 8 - 5
class/Excel.class.php

@@ -1,6 +1,7 @@
 <?php 
 
 require_once (__DIR__.SLASH.'..'.SLASH.'constant.php');
+//Exemple d'utilisation de XLSXWriter : https://github.com/mk-j/PHP_XLSXWriter/tree/master/examples
 require_once (LIB_PATH.'XLSXWriter'.SLASH.'XLSXWriter.class.php');
 require_once (LIB_PATH.'SimpleXLSX'.SLASH.'SimpleXLSX.class.php');
 
@@ -39,8 +40,9 @@ class Excel
 		foreach ($rows as $key => $item) {
 			$itemContent = array();
 			if (isset($mapping)) {
-				foreach ($mapping as $label => $slug) {
-					$header[$label] = 'string';
+				foreach ($mapping as $label => $attributes) {
+					$slug = is_array($attributes) ? (isset($attributes['slug']) ? $attributes['slug'] : '') : $attributes;
+					$header[$label] = (is_array($attributes) && isset($attributes['type'])) ? $attributes['type'] : 'string';
 					isset($item[$slug]) ? array_push($itemContent, $item[$slug]) : array_push($itemContent, '');
 				}
 			} else {
@@ -51,7 +53,6 @@ class Excel
 			array_push($data, $itemContent);
 		}	
 
-		
 		ob_start();
 		$excel = new XLSXWriter();
 		$excel->writeSheetHeader($sheetname, $header, array_merge($style, array('font-style'=> 'bold','border-color' => '#2a2a28')));
@@ -62,6 +63,7 @@ class Excel
 		$output = ob_get_clean();
 		return $output;
 	}
+	
 	/**
 	 * Export de données sur plusieurs feuilles Excel
 	 * @param array $content 	[Contenu à exporter]
@@ -115,8 +117,9 @@ class Excel
 				$itemContent = array();
 
 				if (isset($sheetContent['mapping'])) {
-					foreach ($sheetContent['mapping'] as $label => $slug) {
-						$header[$label] = 'string';
+					foreach ($sheetContent['mapping'] as $label => $attributes) {
+						$slug = is_array($attributes) ? (isset($attributes['slug']) ? $attributes['slug'] : '') : $attributes;
+						$header[$label] = (is_array($attributes) && isset($attributes['type'])) ? $attributes['type'] : 'string';
 						isset($item[$slug]) ? array_push($itemContent, $item[$slug]) : array_push($itemContent, '');
 					}
 				} else {

+ 19 - 14
class/File.class.php

@@ -3,7 +3,7 @@
 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') ,$trimParentDir = true){
+	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']);
@@ -15,10 +15,8 @@ class File{
 		$folderPath = dirname($filePath);
 		$folderAbsolutePath = self::dir().$folderPath;
 		$fileAbsolutePath = $folderAbsolutePath.SLASH.$fileName;
-
-
+		
 		if(!file_exists($folderAbsolutePath)) mkdir($folderAbsolutePath,0755,true);
-
 		if(!move_uploaded_file($_FILES[$index]['tmp_name'], $fileAbsolutePath)) throw new Exception("Impossible de déplacer le fichier envoyé");
 		
 		return array(
@@ -28,6 +26,12 @@ class File{
 		);
 	}
 
+	//récupere le type mime du fichier
+	public static function mime($path){
+		$infos = new finfo();
+		return $infos->file($path, FILEINFO_MIME_TYPE);
+	}
+
 	public static function move($file,$path,$trimParentDir = true){
 		if($trimParentDir){
 			$file = str_replace('..','',$file);
@@ -48,9 +52,9 @@ class File{
 		//New version
 		//Testée, pour le moment pas de pb survenus
 		return array(
-			'absolute' => (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? iconv('UTF-8', 'UTF-8//IGNORE', utf8_encode($fileAbsolutePath)) : $fileAbsolutePath),
-			'relative' => (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? iconv('UTF-8', 'UTF-8//IGNORE', utf8_encode($filePath)) : $filePath),
-			'name' => (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? iconv('UTF-8', 'UTF-8//IGNORE', utf8_encode($fileName)) : $fileName)
+			'absolute' => (get_OS() === 'WIN' ? iconv('UTF-8', 'UTF-8//IGNORE', utf8_encode($fileAbsolutePath)) : $fileAbsolutePath),
+			'relative' => (get_OS() === 'WIN' ? iconv('UTF-8', 'UTF-8//IGNORE', utf8_encode($filePath)) : $filePath),
+			'name' => (get_OS() === 'WIN' ? iconv('UTF-8', 'UTF-8//IGNORE', utf8_encode($fileName)) : $fileName)
 		);
 
 		//Old version
@@ -76,11 +80,12 @@ class File{
 		if($trimParentDir) $path = str_replace('..','',$path);
 		if(!file_exists($path)) throw new Exception("Fichier inexistant :".$path);
 		$stream = file_get_contents($path);
-		if(!isset($mime)){
-			$mime = mime_content_type($path);
-		}
+		
+
+		$mime = !isset($mime) ? self::mime($path) : $mime;
+		
 		if(!isset($name)){
-			$name = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_encode(basename($path)) : basename($path);
+			$name = (get_OS() === 'WIN') ? utf8_encode(basename($path)) : basename($path);
 		}
 	
 		ob_end_clean();
@@ -150,14 +155,14 @@ class File{
 
 	
 	public static function convert_encoding($path){
-		return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? utf8_encode($path) : $path;
+		return get_OS() === 'WIN' ? utf8_encode($path) : $path;
 	}
 	
 	public static function convert_decoding($path){
-		return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? utf8_decode($path) : $path;
+		return get_OS() === 'WIN' ? utf8_decode($path) : $path;
 	}
 
 
 }
 
-?>
+?>

+ 49 - 0
class/History.class.php

@@ -0,0 +1,49 @@
+<?php
+/**
+ * Define a history.
+ * @author  Valentin CARRUESCO
+ * @category Core
+ * @license copyright
+ */
+class History extends Entity{
+
+	public $id;
+	public $scope; //Scope (plugin, page...) (Texte)
+	public $uid; //Item ID/UID concerné (Texte)
+	public $comment; //Commentaire (Texte Long)
+	public $type; //Type d'historique (Texte)
+	public $meta; //Meta données (Texte Long)
+	public $sort; //Ordre (int)
+	
+	const TYPE_COMMENT = 'comment';
+	const TYPE_EDIT = 'edit';
+	const TYPE_DELETE = 'delete';
+
+	protected $TABLE_NAME = 'history';
+	public $fields = array(
+		'id' => 'key',
+		'scope' => 'string',
+		'uid' => 'string',
+		'comment' => 'longstring',
+		'type' => 'string',
+		'sort' => 'int',
+		'meta' => 'longstring'
+	);
+
+	public $links = array(
+	);
+
+	//Colonnes indexées
+	public $indexes = array();
+
+	public static function types($key = null){
+		$types = array(
+			self::TYPE_COMMENT => array('slug'=>self::TYPE_COMMENT,'label' => 'Commentaire','icon' => 'far fa-comments','color' => '#6ab04c'),
+			self::TYPE_EDIT => array('slug'=>self::TYPE_EDIT,'label' => 'Edition','icon' => 'fas fa-marker','color' => '#686de0'),
+			self::TYPE_DELETE => array('slug'=>self::TYPE_DELETE,'label' => 'Suppression','icon' => 'fas fa-trash-restore','color' => '#eb4d4b')
+		);
+		if(!isset($key)) return $types;
+		return isset($types[$key]) ? $types[$key] : array('slug'=>'undefined','label'=>'','icon'=>'','color'=>'');
+	}
+}
+?>

+ 35 - 25
class/Image.class.php

@@ -5,7 +5,7 @@ class Image{
 	public static function toJpg($path){
 
 		$infos = pathinfo($path);
-		if($infos['extension']=='jpg' || $infos['extension']=='jpeg') return;
+		if($infos['extension']=='jpg' || $infos['extension']=='jpeg') return $path;
 	
 		//Make image with white background instead of black when converting png to jpg
 		$input = self::resource($path);
@@ -14,10 +14,12 @@ class Image{
 		$white = imagecolorallocate($output,  255, 255, 255);
 		imagefilledrectangle($output, 0, 0, $width, $height, $white);
 		imagecopy($output, $input, 0, 0, 0, 0, $width, $height);
+		$newPath = $infos['dirname'].SLASH.$infos['filename'].'.jpg';
 	    // quality is a value from 0 (worst) to 100 (best)
-		imagejpeg($output, $infos['dirname'].SLASH.$infos['filename'].'.jpg', 100);
+		imagejpeg($output, $newPath, 100);
 
 		unlink($path);
+		return $newPath;
 	}
 
 	public static function toPng($path){
@@ -37,25 +39,26 @@ class Image{
 		unlink($path);
 	}
 
-	public static function resize($path,$width,$height,$keepRatio = true){
+	public static function resize($path,$width,$height,$keepRatio = true,$forceResize = true){
 		$resource = self::resource($path);
 		$infos = pathinfo($path);
 
-		$oldX =   imageSX($resource);
-    	$oldY =   imageSY($resource);
+		$oldX = imageSX($resource);
+    	$oldY = imageSY($resource);
 
-    	if($oldX > $oldY) 
-	    {
+    	// Ne pas resize une image plus petite
+    	if (!$forceResize && $oldX <= $width && $oldY <= $height) {
+    		return;
+    	}
+    	if($oldX > $oldY) {
 	        $thumb_w    =   $width;
 	        $thumb_h    =   $oldY*($height/$oldX);
 	    }
-	    if($oldX < $oldY) 
-	    {
+	    if($oldX < $oldY) {
 	        $thumb_w    =   $oldX*($width/$oldY);
 	        $thumb_h    =   $height;
 	    }
-	    if($oldX == $oldY) 
-	    {
+	    if($oldX == $oldY) {
 	        $thumb_w    =   $width;
 	        $thumb_h    =   $height;
 	    }
@@ -67,17 +70,26 @@ class Image{
 
 	    $dst_resource = ImageCreateTrueColor($thumb_w,$thumb_h);
     	
-    	if($infos['extension']=='png') {
-    		$resource = ImageCreateFromPNG($path);
-    		imagealphablending($dst_resource, false);
-	    	imagecopyresampled($dst_resource,$resource,0,0,0,0,$thumb_w,$thumb_h,$oldX,$oldY); 
-    		imagesavealpha($dst_resource, true);
-    		$result = imagepng($dst_resource, $path, 6);
-	    }
-	    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,80);
-	    }
+    	switch (mb_strtolower($infos['extension'])) {
+    	    case 'png':
+    	        $resource = ImageCreateFromPNG($path);
+    	        imagealphablending($dst_resource, false);
+    	        imagecopyresampled($dst_resource,$resource,0,0,0,0,$thumb_w,$thumb_h,$oldX,$oldY); 
+    	        imagesavealpha($dst_resource, true);
+    	        $result = imagepng($dst_resource, $path, 4);
+    	    break;
+
+    	    case 'jpg':
+    	    case 'jpeg':
+    	        imagecopyresampled($dst_resource,$resource,0,0,0,0,$thumb_w,$thumb_h,$oldX,$oldY); 
+    	        $result = imagejpeg($dst_resource,$path,80);
+    	    break;
+    	    
+    	    default:
+	    	    //On ne peut pas resize les GIF (sans faire une usine à gaz)
+	    	    //On ne peut resize les BMP que depuis la v7.0 de PHP
+    	    break;
+    	}
 	    imagedestroy($dst_resource); 
     	imagedestroy($resource);
 	}
@@ -95,10 +107,8 @@ class Image{
 			case 'gif':
 				return imagecreatefromgif($path);
 			break;
-			case 'bmp':
-				return imagecreatefrombmp($path);
 			default:
-			return 0;
+				return 0;
 			break;
 		}
 	}

+ 1 - 1
class/Log.class.php

@@ -16,7 +16,7 @@ class Log extends Entity
         'label' => 'longstring',
         'category' => 'string',
         'ip' => 'string',
-        );
+    );
 
     public function label(){
         return preg_replace_callback('|^\[([^\]]*)\](.*)|i', function($matches){

+ 1 - 1
class/Mail.class.php

@@ -83,7 +83,7 @@ class Mail{
 			} else {
 				$stream .= 'Content-type:'.$attachment['type'].';name="'.utf8_decode($attachment['name']).'"'."\r\n";
 				$stream .= 'Content-transfer-encoding:base64'."\r\n";
-				$stream .= 'Content-Disposition:attachement; filename="'.utf8_decode($attachment['name']).'"'."\n\n";
+				$stream .= 'Content-Disposition:attachment; filename="'.utf8_decode($attachment['name']).'"'."\n\n";
 				$stream .= chunk_split(base64_encode($attachment['stream']))."\r\n";
 			}
 		endforeach;

+ 40 - 28
class/Pdf.class.php

@@ -4,7 +4,10 @@
 	Fonctionne sous linux et windows, necessite les binaires wkhtml placés dans "erp\lib\wkhtmltopdf"
 	
 	Sous linux : 
-	sudo apt-get install wkhtmltopdf
+	sudo wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.4/wkhtmltox-0.12.4_linux-generic-amd64.tar.xz
+	sudo tar xvf wkhtmltox-0.12.4_linux-generic-amd64.tar.xz
+	sudo mv wkhtmltox/bin/wkhtmlto* /usr/bin/
+	sudo ln -nfs /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
 	sudo chmod a+x /usr/local/bin/wkhtmltopdf
 	sudo apt-get install openssl build-essential xorg libssl-dev
 	sudo apt-get install xvfb
@@ -83,9 +86,18 @@ class Pdf{
 			return $return;
 		},$fdf);
 
-		$command = 'echo "'.str_replace('"','\"',$fdf).'"| pdftk '.$file.' fill_form - output -';
+		//$command = 'echo "'.str_replace('"','\"',$fdf).'"| pdftk '.$file.' fill_form - output -';
+		$fdfPath = File::temp().rand(0,10000).'fdf';
+		$pdfPath = File::temp().rand(0,10000).'pdf';
+		file_put_contents($fdfPath, $fdf);
+		$command = 'pdftk '.$file.' fill_form '.$fdfPath.' output "'.$pdfPath.'"';
 		if($flat) $command.=' flatten';
-		$datastream = shell_exec($command);
+		
+		shell_exec($command);
+		$datastream = file_get_contents($pdfPath);
+		unlink($pdfPath);
+		unlink($fdfPath);
+
 		return $datastream;
 	}
 
@@ -100,51 +112,51 @@ 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();
 
-		$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 = get_OS() === '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 --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 40';
+			$cmd .= ' --header-html '.$headerPath.' --header-spacing 5';
 		}
 		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 30';
+			$cmd .= ' --footer-html '.$footerPath.' --footer-spacing 5';
 		}
 		$cmd .= ' --footer-right "[page] / [toPage]" --footer-font-size 8 ';
 		$cmd .= $bodyPath.' '.$pdfPath;
+		//file_put_contents(__DIR__.SLASH.'filename', $cmd);
 
 		exec($cmd, $outcmd);
 		$stream = file_get_contents($pdfPath);

+ 4 - 4
class/Plugin.class.php

@@ -190,10 +190,10 @@ class Plugin{
 	 Plugin::need('issue/Event,client/Client,client/ClientContact');
 	*/
 	public static function need($selectors){
-		foreach(explode(',',$selectors) as $selector){
-			list($plugin,$class) = explode('/',$selector);
-			require_once(__ROOT__.PLUGIN_PATH.$plugin.SLASH.$class.'.class.php');
-		}
+	    foreach(explode(',',$selectors) as $selector){
+	        list($plugin,$class) = explode('/',trim($selector));
+	        require_once(__ROOT__.PLUGIN_PATH.$plugin.SLASH.$class.'.class.php');
+	    }
 	}
 	
 }

+ 44 - 20
class/User.class.php

@@ -69,14 +69,14 @@ class User extends Entity
                     $users[$existingKey] = $baseUser;
                 }else{
                     $users[] = $baseUser;
-                } 
+                }
             }
             $_SESSION['users'] = serialize($users);
         }
 
         return $users;
     }
-    
+
     public function __sleep(){
         return array_merge(array('rights','ranks','firms','preferences','meta'),array_keys($this->toArray()));
     }
@@ -96,7 +96,7 @@ class User extends Entity
     }
 
     //Lance les exception appropriées en fonction du droit ou des droits spécifiés
-    // ex : User::checkAccess('document','configure');
+    // ex : User::check_access('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);
@@ -113,7 +113,7 @@ class User extends Entity
         return isset($rankIds[$rankId]);
     }
 
-    public function preference($key=null, $value=null){   
+    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]:'';
 
@@ -145,12 +145,12 @@ class User extends Entity
         endforeach;
     }
 
-    
+
     public function loadRights(){
         global $myFirm;
         $this->rights = array();
         if($this->superadmin) return;
-        
+
         if(!isset($this->ranks)) $this->ranks = array();
         if(!isset($myFirm) || !isset($this->ranks[$myFirm->id]) || count($this->ranks[$myFirm->id])==0) return;
 
@@ -179,7 +179,7 @@ class User extends Entity
             if($right->configure) $this->rights[$right->section][$right->firm]['configure'] = true;
         endforeach;
     }
-    
+
     public function getFirms(){
         $this->firms = array();
         foreach(Firm::staticQuery('SELECT f.* FROM {{table}} f LEFT JOIN '.UserFirmRank::tableName().' uf ON uf.firm=f.id WHERE uf.user=?',array($this->login),true) as $firm):
@@ -194,21 +194,35 @@ class User extends Entity
 
     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);
+        if(!$this->check_avatar_path_length()) return $avatar;
+        $files = glob(__ROOT__.FILE_PATH.AVATAR_PATH.self::format_avatar_name($this->login).self::get_avatar_extension_brace(),GLOB_BRACE);
+
         if(count($files)>0){
             if($getPath) return $files[0];
             preg_match("/\.(\w{3,4})$|\?/m", $files[0], $extension);
-            $avatar = 'action.php?action=account_avatar_download&user='.$this->login.'&extension='.$extension[1];
+            $avatar = 'action.php?action=account_avatar_download&user='.urlencode($this->login).'&extension='.$extension[1];
         }
         return $avatar;
     }
 
-    public static function check($login, $password, $loadRight = true) {   
+    public function check_avatar_path_length(){
+        return strlen($this->login) <= OS_path_max_length() - strlen(__ROOT__.FILE_PATH.AVATAR_PATH) - strlen(User::get_avatar_extension_brace());
+    }
+
+    public static function format_avatar_name($text){
+        return iconv('utf-8','windows-1256//IGNORE', $text);
+    }
+
+    public static function get_avatar_extension_brace(){
+        return ".{jpg,png,jpeg,gif}";
+    }
+
+    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));
+        Plugin::callHook("user_login", array(&$user,htmlspecialchars_decode($login),htmlspecialchars_decode($password),$loadRight));
 
         //load from db
         if($user!=false){
@@ -236,15 +250,25 @@ class User extends Entity
         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, $force=false){
+        foreach(User::getAll($loadRight, $force) as $user){
+            if($user->login != $login) continue;
+            return $user;
+        }
         return new User();
     }
 
-    public function fullName()
-    {
-        $fullName = ucfirst($this->firstname).' '.mb_strtoupper($this->name);
+
+    public function lastname(){
+        return mb_strtoupper(htmlspecialchars_decode(mb_strtolower($this->name)));
+    }
+
+    public function firstname(){
+        return htmlspecialchars_decode($this->firstname);
+    }
+
+    public function fullName(){
+        $fullName = ucfirst($this->firstname()).' '.$this->lastname();
         return trim($fullName) != '' ? $fullName : $this->login;
     }
 
@@ -284,7 +308,7 @@ class User extends Entity
         $formats = array();
         foreach (self::password_formats() as $format) {
             $formats[$format['pattern']] = $format;
-        } 
+        }
         $selectedFormats = json_decode($conf->get('password_format'),true);
 
         if(is_array($selectedFormats)){
@@ -358,7 +382,7 @@ class User extends Entity
         if(!isset($this->manager)) return $manager;
         if(is_object($this->manager)) $manager = $this->manager;
         if(is_string($this->manager) && !empty($this->manager)) $manager = User::byLogin($this->manager);
-       
+
         return is_object($manager) ? $manager: new User();
     }
 }

+ 58 - 43
common.php

@@ -2,6 +2,10 @@
 session_name ('erp-core');
 session_start();
 
+//Activation du ssl a travers un reverse proxy
+if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) $_SERVER['HTTPS'] = 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ? 'on' : 'off';
+
+
 $start_time = microtime(TRUE);
 mb_internal_encoding('UTF-8');
 
@@ -28,25 +32,38 @@ $conf->getAll();
 
 //CONFS GÉNÉRALES
 Configuration::setting('configuration-global',array(
-    "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"),
-    '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"),
+   "Gestion des configurations générales :",
+   'home_page' => array("label"=>"Page d'accueil","type"=>"text","legend"=>"Laisser vide pour gérer en automatique","placeholder"=>"eg : index.php?module=example"),
+   'logo_website_header' => array("label"=>"Site web cible au clic sur le logo","type"=>"text","legend"=>"Dans le menu de navigation, laisser vide pour pointer vers l'Accueil du projet", "placeholder"=>"eg : https://example.com"),
+   'show_application_name' => array("label"=>"Afficher le nom du programme","type"=>"checkbox","legend"=>"Dans le menu de navigation"),
+   'show_application_name_footer' => array("label"=>"Afficher le nom du programme", "legend"=>"Dans le pied de page", "type"=>"checkbox"),
+   'show_application_author_footer' => array("label"=>"Afficher le nom de l'éditeur", "legend"=>"Dans le pied de page", "type"=>"checkbox"),
+   'application_author_website_footer' => array("label"=>"Lien vers le site de l'éditeur", "legend"=>"Dans le pied de page, laisser vide pour ne rien afficher", "type"=>"text", "placeholder"=>"eg : https://example.com"),
+   'show_application_documentation_footer' => array("label"=>"Lien vers la documentation utilisateur", "legend"=>"Dans le pied de page, laisser vide pour ne rien afficher", "type"=>"text"),
+   'show_process_time_footer' => array("label"=>"Afficher le temps de traitement", "legend"=>"Dans le pied de page", "type"=>"checkbox"),
+   'hide_header_login' => array("label"=>"Masquer le formulaire de connexion dans le header","type"=>"checkbox","legend"=>"(Barre de menu en haut à droite)","placeholder"=>"6"),
+   "Gestion des clés API du composant <code>location</code> :",
+   'maps_api_suggest_url' => array("label"=>"URL de l'API de Suggestion","type"=>"text","legend"=>"URL de l'API à attaquer pour autocomplétion des adresses","placeholder"=>"eg. http://autocomplete.suggest.api.example.com/..."),
+   'maps_api_geocode_url' => array("label"=>"URL de l'API Geocoder","type"=>"text","legend"=>"URL de l'API à attaquer pour récupérer les détails d'une localisation","placeholder"=>"eg. http://autocomplete.geocoder.api.example.com/..."),
+   'maps_api_id' => array("label"=>"ID de l'application","type"=>"text","legend"=>"Identifiant de l'application API pour le composant location","placeholder"=>"eg. pl0749TULNDW..."),
+   'maps_api_key' => array("label"=>"Clé publique de l'application","type"=>"password","legend"=>"Code / Clé de l'application API pour le composant location","placeholder"=>"eg. db678804676..."),
+   'google_maps_api_key' => array("label"=>"Google Maps API","type"=>"text","legend"=>"Clé API de la console Google Cloud Platform","placeholder"=>"eg. AIzaSyAcnOX3qYl-Fzq..."),
+   "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"),
+	"Identifiant :",
+	'login_forbidden_char' => array("label"=>"Caractères interdits","type"=>"text","legend"=>"<small class='text-danger'> La virgule ','' est par défaut interdite pour tout identifiant</small>","placeholder"=>"eg. <>&!?"),
+	'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_forbidden_char' => array("label"=>"Caractères interdits","type"=>"text","legend"=>"<small class='text-danger'> Aucun caractère n'est par défaut interdit</small>","placeholder"=>"eg. <>&!?"),
+	'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;
+$cacheVersion = SOURCE_VERSION;
 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)){
@@ -62,22 +79,27 @@ if($myUser->login==null && isset($_COOKIE[COOKIE_NAME])){
 	        require_once(PLUGIN_PATH.'activedirectory'.SLASH.'activedirectory.plugin.php');
 	    
 	    $myUser = User::byLogin($cookie->user);
-        if($myUser->origin != 'active_directory'){
+        if(empty($myUser->origin)){
             $myUser->ranks = array();
             $myUser->firms = array();
             $myUser->loadRanks();
             $myUser->loadPreferences();
         }
+        if($myUser->superadmin == 1){
+            foreach(Firm::loadAll() as $firm)
+                $firms[$firm->id] = $firm;
+            $myUser->setFirms($firms);
+        }
         
         $defaultFirm = !empty($myUser->preference('default_firm')) ? $myUser->preferences['default_firm'] : key($myUser->firms);
         $myFirm = isset($myUser->firms[$defaultFirm]) ? $myUser->firms[$defaultFirm]:key($myUser->firms);
+        $myUser->loadRights();
         
         $_SESSION['currentUser'] = serialize($myUser);
         $_SESSION['firm'] = serialize($myFirm);
     }
 }
-
-$myFirm = isset($_SESSION['firm']) ? unserialize($_SESSION['firm']): new Firm();
+$myFirm = isset($_SESSION['firm']) ? unserialize($_SESSION['firm']) : new Firm();
 
 //MENUS
 Plugin::addHook("menu_account", function(&$accountMenu){
@@ -184,14 +206,14 @@ Plugin::addHook("menu_setting", function(&$settingMenu){
 Plugin::addHook("menu_main", function(&$mainMenu) {
 	global $myUser;
 	
+	if(!$myUser->connected()) return;
 	$mainMenu[] = array(
 		'sort' =>0,
-		'icon' => 'fas fa-home',
+		'icon' => 'fas fa-fw fa-home',
 		'label' => 'Accueil',
 		'url' => 'index.php',
 		'color' => '#383838'
 	);
-	if(!$myUser->connected()) return;
 	$settingMenu = array();
 	Plugin::callHook("menu_setting", array(&$settingMenu));
 });
@@ -209,42 +231,35 @@ Plugin::addHook("menu_user", function(&$userMenu){
 		    }
 		}
 	}
-
-	if(count($rankLabels)!=0){
-	    $rankLabels = '<div class="firm-ranks"><ul><li>'.implode('</li><li>',$rankLabels).'</li></ul></div>';
-	} else {
-	    $rankLabels = '';
-	}
+    $ranksHtml = count($rankLabels)!=0 ? '<div class="firm-ranks mt-1"><ul><li>'.implode('</li><li>',$rankLabels).'</li></ul></div>' : '';
 
 	$userMenu[]= array(
-		'sort' =>-2,
-		'custom' => "<div class='firm-item' onclick='event.stopPropagation();'><small>Rang : ".$rankLabels."</small></div><div class='dropdown-divider'></div>",
+		'sort' => -2,
+		'custom' => "<div class='firm-item' onclick='event.stopPropagation();'><small>Rang : ".$ranksHtml."</small></div><div class='dropdown-divider'></div>",
 	);
 
 	if(count($myUser->firms)>1){
-		$userIcon = 'far fa-user';
+		$userIcon = 'far fa-fw fa-user';
 		$options = '';
 
-		foreach ($myUser->firms as $firm) {
+		foreach ($myUser->firms as $firm)
 			$options .= '<option '.($myFirm->id == $firm->id ? "selected='selected'":"").' value="'.$firm->id.'">'.$firm->label.'</option>';
-		}
 		
 		$userMenu[]= array(
-			'sort' =>1,
-			'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>",
+			'sort' => 1,
+			'custom' => "<div class='firm-item mt-2' onclick='event.stopPropagation();'><small class='mb-1'>É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 {
-		$userIcon = 'fas fa-user';
+		$userIcon = 'fas fa-fw fa-user';
 		$userMenu[]= array(
-			'sort' =>-1,
+			'sort' => -1,
 			'custom' => "<div class='firm-item' onclick='event.stopPropagation();'><small>Établissement : ".$myFirm->label."</small></div><div class='dropdown-divider'></div>",
 		);
 	}
 
 	if($myUser->can('account','read'))
 		$userMenu[]= array(
-			'sort' =>0,
+			'sort' => 0,
 			'label' => 'Mon compte',
 			'icon' => $userIcon,
 			'url' => 'account.php'
@@ -252,15 +267,15 @@ Plugin::addHook("menu_user", function(&$userMenu){
 
 	if($myUser->can('setting_global', 'read'))
 		$userMenu[]= array(
-			'sort' =>1,
-			'icon' => 'fas fa-cog',
+			'sort' => 1,
+			'icon' => 'fas fa-fw fa-cog',
 			'label' => 'Réglages',
 			'url' => 'setting.php'
 		);
 
 	$userMenu[]= array(
-		'sort' =>100,
-		'icon' => 'fas fa-sign-out-alt',
+		'sort' => 100,
+		'icon' => 'fas fa-fw fa-sign-out-alt',
 		'label' => 'Déconnexion',
 		'url' => 'action.php?action=logout'
 	);
@@ -305,4 +320,4 @@ Plugin::addHook("cron",function(){
 
 Plugin::includeAll();
 
-?>
+?>

+ 41 - 2
connector/Mysql.class.php

@@ -10,8 +10,17 @@
 class Mysql
 {
 	const label = 'MySQL';
-	const connection = 'mysql:host={{BASE_HOST}};dbname={{BASE_NAME}}';
+	const connection = 'mysql:host={{host}};dbname={{name}}';
 	const description = 'Base robuste authentifiée necessitant un serveur Mysql (Conseillé)';
+	const table_escape = '`';
+	const column_escape = '`';
+
+	public static function pdo_attributes(){
+		return array(
+			PDO::ATTR_PERSISTENT => true,
+			PDO::ATTR_ERRMODE=> PDO::ERRMODE_EXCEPTION
+		);
+	}
 
 	public static function fields(){
 		return array(
@@ -22,6 +31,26 @@ class Mysql
 		);
 	}
 
+	public static function processField(&$field,&$value,&$values,&$i){
+		if($field['operator'] == 'IN'){
+            $field['tag'] = array(); 
+            foreach (explode(',',$value) as $v2) {
+                $tag = ':'.$i;
+                $field['tag'][]= $tag;
+                $values[$tag] = $v2;
+                $i++;
+            }
+            $field['tag'] = implode(',',$field['tag']);
+            $field['operator'] = 'IN(';
+            $field['postoperator'] = ')';
+        }else{
+            $tag = ':'.$i;
+            $field['tag'] = $tag;
+            $values[$tag] = $value;
+            $i++;
+        }
+	}
+
 	public static function types(){
 		$types = array();
 		$types['string']  = 'VARCHAR(225) CHARACTER SET utf8 COLLATE utf8_general_ci';
@@ -53,7 +82,7 @@ class Mysql
 		return $sql;
 	}
 	public static function update(){
-		$sql = 'UPDATE `{{table}}` SET {{?fields}} {{:fields}}`{{key}}`={{value}} {{;}} , {{/;}} {{/:fields}} {{/?fields}} {{?filters}}WHERE {{:filters}}`{{key}}`{{operator}}{{value}} {{;}} AND {{/;}} {{/:filters}} {{/?filters}}';
+		$sql = 'UPDATE `{{table}}` SET {{?fields}} {{:fields}}`{{key}}`={{value}} {{;}} , {{/;}} {{/:fields}} {{/?fields}} {{?filters}}WHERE {{:filters}}`{{key}}`{{operator}} {{value}} {{postoperator}} {{;}} AND {{/;}} {{/:filters}} {{/?filters}}';
 		return $sql;
 	}
 	public static function insert(){
@@ -89,6 +118,16 @@ class Mysql
 		WHERE table_schema=DATABASE() AND table_name='{{table}}' AND index_name='{{index_name}}'";
 		return $sql;
 	}
+
+	public static function show_tables(){
+		$sql = 'SHOW TABLES';
+		return $sql;
+	}
+
+	public static function show_columns(){
+	  $sql = 'SELECT COLUMN_NAME `column`,DATA_TYPE `type` FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = "{{table}}"';
+	  return $sql;
+	}
 }
 
 ?>

+ 145 - 0
connector/Oracle.class.php

@@ -0,0 +1,145 @@
+<?php
+
+/**
+ * Define SQL for Mysql database system
+ * @author valentin carruesco
+ * @category Core
+ * @license copyright
+ */
+
+class Oracle
+{
+	const label = 'Oracle';
+	const connection = 'odbc:{{name}}';
+	const description = 'Base  authentifiée necessitant un serveur Oracle Database XE ou Oracle Database Entreprise, creation de source ODBC à prévoir';
+	const table_escape = '"';
+	const column_escape = '"';
+
+	public static function pdo_attributes(){
+		return array(
+			PDO::ATTR_PERSISTENT => true
+		);
+	}
+
+	public static function fields(){
+		return array(
+			array('id'=>'name','label'=>'Nom de la source ODBC','default'=>'','comment'=>''),
+			array('id'=>'login','label'=>'Identifiant','default'=>'','comment'=>''),
+			array('id'=>'password','label'=>'Mot de passe','default'=>'','comment'=>'')
+		);
+	}
+
+	public static function processField(&$field,&$value,&$values,&$i){
+		
+		if($field['type'] == 'date'){
+			
+			$field['operator'].= ' to_date( ';
+			$field['postoperator'].= ',\'YYYY-MM-DD\')';
+		}
+		switch($field['operator']){
+			case 'IN':
+	            $field['tag'] = array(); 
+	            foreach (explode(',',$value) as $v2) {
+	                $tag = ':'.$i;
+	                $field['tag'][]= $tag;
+	                $values[$tag] = $v2;
+	                $i++;
+	            }
+	            $field['tag'] = implode(',',$field['tag']);
+	            $field['operator'] = 'IN(';
+	            $field['postoperator'] = ')';
+	        break;
+	        default:
+	            $tag = ':'.$i;
+	            $field['tag'] = $tag;
+	            $values[$tag] = $value;
+	            $i++;
+        	break;
+    	}
+
+	}
+
+	public static function types(){
+		$types = array();
+		$types['string']  = 'VARCHAR2(225) CHARACTER SET utf8 COLLATE utf8_general_ci';
+		$types['longstring'] = 'TEXT CHARACTER SET utf8 COLLATE utf8_general_ci';
+		$types['key'] = 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY';
+		$types['object'] = $types['timestamp'] = $types['datetime'] = $types['date'] = $types['int'] = 'INT(11)';
+		$types['boolean'] = 'tinyint(1) NOT NULL DEFAULT \'0\'';
+		$types['blob'] = ' BLOB';
+		$types['float'] = 'FLOAT';
+		$types['decimal'] = 'DECIMAL(20,2)';
+		$types['default'] = 'TEXT CHARACTER SET utf8 COLLATE utf8_general_ci';
+		return $types;
+	}
+
+
+	public static function beforeTransaction($pdo){
+		$pdo->exec("set names utf8");
+	}
+	
+	public static function select(){
+		$sql = 'SELECT {{:selected}}{{value}}{{;}},{{/;}}{{/:selected}} FROM "{{table}}" {{?joins}}{{:joins}}LEFT JOIN "{{jointable2}}" ON "{{jointable1}}".{{field1}}= "{{jointable2}}".{{field2}} {{/:joins}}{{/?joins}} {{?filter}} WHERE {{:filter}} "{{table}}"."{{key}}" {{operator}} {{value}} {{postoperator}} {{;}} AND {{/;}} {{/:filter}} {{/?filter}} {{?orderby}}ORDER BY {{:orderby}}{{value}}{{;}},{{/;}}{{/:orderby}} {{/?orderby}}{{?limit}}FETCH NEXT {{:limit}}{{value}}{{;}}{{/;}} ROWS ONLY{{/:limit}}{{/?limit}}';
+		return $sql;
+	}
+	public static function delete(){
+		$sql = 'DELETE FROM "{{table}}" {{?filter}}WHERE {{:filter}}"{{key}}"{{operator}}{{value}} {{;}} AND {{/;}} {{/:filter}} {{/?filter}} {{?limit}}LIMIT {{:limit}}{{value}}{{;}},{{/;}}{{/:limit}}{{/?limit}}';
+		return $sql;
+	}
+	public static function count(){
+		$sql = 'SELECT COUNT({{selected}}) number FROM "{{table}}" {{?filter}}WHERE {{:filter}}"{{key}}"{{operator}}{{value}}{{postoperator}} {{;}} AND {{/;}} {{/:filter}} {{/?filter}}';
+		return $sql;
+	}
+	public static function update(){
+		$sql = 'UPDATE "{{table}}" SET {{?fields}} {{:fields}}"{{key}}"={{value}} {{;}} , {{/;}} {{/:fields}} {{/?fields}} {{?filters}}WHERE {{:filters}}"{{key}}"{{operator}} {{value}} {{postoperator}} {{;}} AND {{/;}} {{/:filters}} {{/?filters}}';
+		return $sql;
+	}
+	public static function insert(){
+		$sql = 'INSERT INTO  "{{table}}" ({{?fields}} {{:fields}}"{{key}}" {{;}} , {{/;}} {{/:fields}} {{/?fields}})VALUES({{?fields}} {{:fields}}{{value}} {{;}} , {{/;}} {{/:fields}} {{/?fields}})';
+		return $sql;
+	}
+	public static function create(){
+		$sql = 'CREATE TABLE IF NOT EXISTS "{{table}}" ({{?fields}} {{:fields}}"{{key}}" {{value}}{{;}} , {{/;}} {{/:fields}} {{/?fields}}) DEFAULT CHARSET=utf8;';
+		return $sql;
+	}
+	public static function drop(){
+		$sql = 'DROP TABLE IF EXISTS "{{table}}";';
+		return $sql;
+	}
+
+	public static function truncate(){
+		$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;
+	}
+
+	public static function show_tables(){
+		$sql = 'SELECT table_name FROM user_tables ORDER BY table_name;';
+		return $sql;
+	}
+	public static function show_columns(){
+	  $sql = "SELECT COLUMN_NAME \"column\",DATA_TYPE \"type\" 
+		FROM user_tab_cols
+		WHERE table_name = '{{table}}' AND USER_GENERATED = 'YES'";
+	  return $sql;
+	}
+
+
+}
+
+?>

+ 135 - 0
connector/SqlServer.class.php

@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * Define SQL for SQL Server database system
+ * @author valentin carruesco
+ * @category Core
+ * @license copyright
+ */
+
+class SqlServer
+{
+	const label = 'SQLServer';
+	const connection = 'sqlsrv:Server={{host}};Database={{name}}';
+	const description = 'Base microsoft SQL Server authentifiée';
+	const table_escape = '"';
+	const column_escape = '"';
+
+	public static function pdo_attributes(){
+		return array(
+			PDO::ATTR_PERSISTENT => true,
+			PDO::ATTR_ERRMODE=> PDO::ERRMODE_EXCEPTION
+		);
+	}
+
+	public static function fields(){
+		return array(
+			array('id'=>'host','label'=>'Serveur','default'=>'localhost','comment'=>''),
+			array('id'=>'login','label'=>'Identifiant','default'=>'','comment'=>''),
+			array('id'=>'password','label'=>'Mot de passe','default'=>'','comment'=>''),
+			array('id'=>'name','label'=>'Nom de la base','default'=>'','comment'=>'')
+		);
+	}
+
+	public static function processField(&$field,&$value,&$values,&$i){
+		if($field['operator'] == 'IN'){
+            $field['tag'] = array(); 
+            foreach (explode(',',$value) as $v2) {
+                $tag = ':'.$i;
+                $field['tag'][]= $tag;
+                $values[$tag] = $v2;
+                $i++;
+            }
+            $field['tag'] = implode(',',$field['tag']);
+            $field['operator'] = 'IN(';
+            $field['postoperator'] = ')';
+        }else{
+            $tag = ':'.$i;
+            $field['tag'] = $tag;
+            $values[$tag] = $value;
+            $i++;
+        }
+	}
+
+	public static function types(){
+		$types = array();
+		$types['string']  = 'VARCHAR(225) CHARACTER SET utf8 COLLATE utf8_general_ci';
+		$types['longstring'] = 'TEXT CHARACTER SET utf8 COLLATE utf8_general_ci';
+		$types['key'] = 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY';
+		$types['object'] = $types['timestamp'] = $types['datetime'] = $types['date'] = $types['int'] = 'INT(11)';
+		$types['boolean'] = 'tinyint(1) NOT NULL DEFAULT \'0\'';
+		$types['blob'] = ' BLOB';
+		$types['float'] = 'FLOAT';
+		$types['decimal'] = 'DECIMAL(20,2)';
+		$types['default'] = 'TEXT CHARACTER SET utf8 COLLATE utf8_general_ci';
+		return $types;
+	}
+
+	public static function beforeTransaction($pdo){
+		$pdo->exec("set names utf8");
+	}
+	
+	public static function select(){
+		$sql = 'SELECT {{:selected}}{{value}}{{;}},{{/;}}{{/:selected}} FROM "{{table}}" {{?joins}}{{:joins}}LEFT JOIN "{{jointable2}}" ON "{{jointable1}}".{{field1}}= "{{jointable2}}".{{field2}} {{/:joins}}{{/?joins}} {{?filter}} WHERE {{:filter}} "{{table}}"."{{key}}" {{operator}} {{value}} {{postoperator}} {{;}} AND {{/;}} {{/:filter}} {{/?filter}} {{?orderby}}ORDER BY {{:orderby}}{{value}}{{;}},{{/;}}{{/:orderby}} {{/?orderby}} {{?limit}}LIMIT {{:limit}}{{value}}{{;}},{{/;}}{{/:limit}}{{/?limit}}';
+		return $sql;
+	}
+	public static function delete(){
+		$sql = 'DELETE FROM "{{table}}" {{?filter}}WHERE {{:filter}}"{{key}}"{{operator}}{{value}} {{;}} AND {{/;}} {{/:filter}} {{/?filter}} {{?limit}}LIMIT {{:limit}}{{value}}{{;}},{{/;}}{{/:limit}}{{/?limit}}';
+		return $sql;
+	}
+	public static function count(){
+		$sql = 'SELECT COUNT({{selected}}) number FROM "{{table}}" {{?filter}}WHERE {{:filter}}"{{key}}"{{operator}}{{value}}{{postoperator}} {{;}} AND {{/;}} {{/:filter}} {{/?filter}}';
+		return $sql;
+	}
+	public static function update(){
+		$sql = 'UPDATE "{{table}}" SET {{?fields}} {{:fields}}"{{key}}"={{value}} {{;}} , {{/;}} {{/:fields}} {{/?fields}} {{?filters}}WHERE {{:filters}}"{{key}}"{{operator}} {{value}} {{postoperator}} {{;}} AND {{/;}} {{/:filters}} {{/?filters}}';
+		return $sql;
+	}
+	public static function insert(){
+		$sql = 'INSERT INTO  "{{table}}" ({{?fields}} {{:fields}}"{{key}}" {{;}} , {{/;}} {{/:fields}} {{/?fields}})VALUES({{?fields}} {{:fields}}{{value}} {{;}} , {{/;}} {{/:fields}} {{/?fields}})';
+		return $sql;
+	}
+	public static function create(){
+		$sql = 'CREATE TABLE IF NOT EXISTS "{{table}}" ({{?fields}} {{:fields}}"{{key}}" {{value}}{{;}} , {{/;}} {{/:fields}} {{/?fields}}) DEFAULT CHARSET=utf8;';
+		return $sql;
+	}
+	public static function drop(){
+		$sql = 'DROP TABLE IF EXISTS "{{table}}";';
+		return $sql;
+	}
+
+	public static function truncate(){
+		$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;
+	}
+
+	public static function show_tables(){
+		$sql = 'SELECT sobjects.name FROM sysobjects sobjects WHERE sobjects.xtype = \'U\'';
+		return $sql;
+	}
+
+	public static function show_columns(){
+	  $sql = "select * from information_schema.columns where table_name = '{{table}}'";
+	  return $sql;
+	}
+	
+
+}
+
+?>

+ 43 - 5
connector/Sqlite.class.php

@@ -10,14 +10,43 @@
 class Sqlite
 {
 	const label = 'SQLite3';
-	const connection = 'sqlite:{{ROOT}}db/{{BASE_NAME}}.db';
+	const connection = 'sqlite:{{ROOT}}db/{{name}}.db';
 	const description = 'Base légere monofichier sans authentification, simple d\'utilisation/installation mais limitée en performances';
+	const table_escape = '"';
+	const column_escape = '"';
+
+	public static function pdo_attributes(){
+		return array(
+			PDO::ATTR_PERSISTENT => true,
+			PDO::ATTR_ERRMODE=> PDO::ERRMODE_EXCEPTION
+		);
+	}
 
 	public static function fields(){
 		return array(
 			array('id'=>'name','label'=>'Nom de la base','default'=>'.database','comment'=>'')
 		);
 	}
+
+	public static function processField(&$field,&$value,&$values,&$i){
+		if($field['operator'] == 'IN'){
+            $field['tag'] = array(); 
+            foreach (explode(',',$value) as $v2) {
+                $tag = ':'.$i;
+                $field['tag'][]= $tag;
+                $values[$tag] = $v2;
+                $i++;
+            }
+            $field['tag'] = implode(',',$field['tag']);
+            $field['operator'] = 'IN(';
+            $field['postoperator'] = ')';
+        }else{
+            $tag = ':'.$i;
+            $field['tag'] = $tag;
+            $values[$tag] = $value;
+            $i++;
+        }
+	}
 	
 	public static function types(){
 		$types = array();
@@ -48,7 +77,7 @@ class Sqlite
 		return $sql;
 	}
 	public static function update(){
-		$sql = 'UPDATE {{table}} SET {{?fields}} {{:fields}}"{{key}}"={{value}} {{;}}, {{/;}}{{/:fields}} {{/?fields}} {{?filters}}WHERE {{:filters}}{{key}}{{operator}}{{value}} {{;}} AND {{/;}} {{/:filters}} {{/?filters}}';
+		$sql = 'UPDATE {{table}} SET {{?fields}} {{:fields}}"{{key}}"={{value}} {{;}}, {{/;}}{{/:fields}} {{/?fields}} {{?filters}}WHERE {{:filters}}{{key}}{{operator}}{{value}}{{postoperator}} {{;}} AND {{/;}} {{/:filters}} {{/?filters}}';
 		return $sql;
 	}
 	public static function insert(){
@@ -70,21 +99,30 @@ class Sqlite
 	}
 
 	public static function create_index(){
-		$sql = 'CREATE INDEX  `{{index_name}}` ON `{{table}}` ({{column}})';
+		$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}}`';
+		$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`";
+		$sql = "SELECT 0 \"exists\"";
 		return $sql;
 	}
 
+	public static function show_tables(){
+		$sql = 'select name from SQLite_master WHERE type="table"';
+		return $sql;
+	}
+
+	public static function show_columns(){
+	  $sql = "PRAGMA table_info({{table}});";
+	  return $sql;
+	}
 
 }
 

+ 20 - 7
constant-sample.php

@@ -10,14 +10,25 @@ define('LIB_PATH',__ROOT__.'lib'.SLASH);
 
 define('AVATAR_PATH','avatar'.SLASH);
 
-define('BASE_SGBD','{{BASE_SGBD}}');
-define('BASE_HOST','{{BASE_HOST}}');
-define('BASE_NAME','{{BASE_NAME}}');
-define('BASE_LOGIN','{{BASE_LOGIN}}');
-define('BASE_PASSWORD','{{BASE_PASSWORD}}');
+global $databases_credentials;
+$databases_credentials = array(
+	'local' => array(
+		'connector' => '{{connector}}',
+		'host' => '{{host}}',
+		'name' => '{{name}}',
+		'login' => '{{login}}',
+		'password' => '{{password}}',
+	),
+	// 'db2' => array(
+	// 	'connector' => 'Oracle',
+	// 	'name' => 'my ODBC name',
+	// 	'login' => 'login',
+	// 	'password' => 'password',
+	// )
+);
 
-define('ROOT_URL','{{ROOT_URL}}');
-define('CRYPTKEY','{{CRYPT_KEY}}');
+define('ROOT_URL','{{root}}');
+define('CRYPTKEY','{{cryptKey}}');
 //logs toutes les requetes sans formattage
 define('BASE_DEBUG',false);
 
@@ -25,6 +36,7 @@ define('COOKIE_NAME','erp-core-cookie');
 
 define('PROGRAM_NAME','Sys1 ERP');
 define('PROGRAM_UID','sys1/erp-core');
+define('PROGRAM_AUTHOR','SYS1');
 define('PROGRAM_TECHNICIAN','valentin.morreel');
 //Windows
 define('REFERENCE_URL','https://projet.sys1.fr/action.php?action=reference_save_project');
@@ -34,4 +46,5 @@ define('REFERENCE_URL','https://projet.sys1.fr/action.php?action=reference_save_
 define('SOURCE_VERSION','1.0');
 define('BASE_VERSION','1.0');
 
+//define('CACHE_API',true); //permet une simulation de l'utilisation de l'api hors ligne
 ?>

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


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


File diff suppressed because it is too large
+ 732 - 108
css/main.css


+ 143 - 110
footer.php

@@ -6,136 +6,167 @@ global $myUser,$conf;
 $scheme = isset($scheme) ? $scheme : define_url_scheme();
 $mediaRoot = isset($mediaRoot) ? $mediaRoot : define_media_root();
 $loadingTime = isset($start_time) ? (round(microtime(TRUE) - $start_time,5)) : '';
-$cacheVersion = isset($cacheVersion) ? $cacheVersion : 1;
+$cacheVersion = isset($cacheVersion) ? $cacheVersion : SOURCE_VERSION;
 ?>
 	<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>
+			<span class="text-muted">
+			<?php 
+				echo $conf->get('show_application_name_footer') ? PROGRAM_NAME.' V'.SOURCE_VERSION.'b'.BASE_VERSION.' ' : '';
+				if($conf->get('show_application_author_footer')){
+					if(!empty($conf->get('application_author_website_footer')))
+						echo 'by <a href="'.$conf->get('application_author_website_footer').'" target="_blank">@'.PROGRAM_AUTHOR.'</a>';
+					else
+						echo 'by @'.PROGRAM_AUTHOR;
+				}				
+				echo !empty($conf->get('show_application_documentation_footer')) && file_exists('file/guide/'.$conf->get('show_application_documentation_footer')) ? ' | <a href="file/guide/'.$conf->get('show_application_documentation_footer').'" target="_blank"><i class="far fa-file-pdf"></i> Documentation</a>' : '';
+				echo $conf->get('show_process_time_footer') ? ' - '.$loadingTime : '';
+			?>
+			</span>
 		</div>
-		<div id="toTheTop" title="Retour en haut de page"></div>
+		<div id="scroll-top" title="Retour en haut de page"></div>
 	</footer>
 
 	<!-- Composant filtre -->
-	<div class="filter-box hidden">
+	<div class="advanced-search-box hidden">
 		<div class="input-group simple-search">
 			<div class="input-group-prepend">
-				<div class="input-group-text">Recherche</div>
+				<div class="input-group-text data-search-label">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 class="input-group-append hidden">
+				<div class="btn btn-info btn-search" title="Rechercher"><i class="fas fa-search"></i> Rechercher</div>
 			</div>
 		</div>
-		<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>
+		<div class="advanced-search">
+		    <ul class="criterias group">
+		            <li class="condition hidden">
+		                <span class="filter-column">
+		                	<select class="form-control">
+		                	{{#columns}}
+		                		<option value="{{value}}" data-filter-type="{{type}}">{{label}}</option>
+		                	{{/columns}}
+		                </select>
+		                </span> 
+		                <span class="filter-operator"></span> 
+		                <span class="filter-value"></span> 
+		                <span class="filter-option">
+		                	<i title="Déplacer la ligne" class="fas fa-arrows-alt btn-move"></i>
+		                    <i title="Déplacer dans le groupe supérieur" class="btn-unindent fas fa-angle-left"></i>
+		                    <i title="Déplacer dans un sous-groupe" class="btn-indent fas fa-angle-right"></i> 
+		                    <i title="Supprimer la ligne" class="btn-delete far fa-trash-alt"></i>
+		                    <i title="Ajouter une ligne après" class="btn-add fas fa-plus"></i>
+		                </span>
+		                <select class="filter-join"><option value="and">ET</option><option value="or">OU</option></select>
+		            </li>
+		        </li>
+		    </ul>
+		</div>
+		<div class="options">
+	    	<div class="btn btn-light btn-small btn-search"><i class="fas fa-search"></i> Rechercher</div>
+	       	 <div class="btn btn-light btn-small advanced-button-search pointer" title="Mode simple / avancé"><i class="fas fa-filter"></i></div>
+	        <div class="dropdown preferences btn-light">
+	            <button class="btn btn-light btn-small dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+	                <i class="fas fa-user-cog"></i>
+	            </button>
+	            <div class="dropdown-menu tools py-1" aria-labelledby="dropdownMenuButton">
+	                <a class="dropdown-item py-1 px-2 text-success hidden btn-search-save pointer"><i class="far fa-save"></i> Enregistrer la recherche</a>
+	                <a class="dropdown-item py-1 px-2 text-muted btn-search-clean pointer"><i class="fas fa-broom"></i> Nettoyer la recherche</a>
+	                <div class="dropdown-divider my-2 hidden"></div>
+	                <a class="dropdown-item py-1 px-2 text-danger hidden btn-search-reset pointer text-center"><i class="fas fa-trash-alt"></i> Supprimer la recherche</a>
+	            </div>
+	        </div>
+	       
+	    </div>
+	    <div class="clear"></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="=">É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 class="hidden">
+		<div class="filter-value-block" data-value-type="text">
+			<select class="form-control filter-operator border-0 text-primary">
+				<option value="LIKE">Contient</option>
+				<option value="NOT LIKE">Ne contient pas</option>
+				<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" placeholder="Mot clé" class="form-control filter-value">
+		</div>
+		<div class="filter-value-block" data-value-type="user" data-value-selector=".filter-value:eq(1)" >
+			<select class="form-control filter-operator border-0 text-primary">
+				<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-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">
+				<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" 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">
+				<option value="=">Le</option>
+				<option value="<">Avant</option>
+				<option value=">">Après</option>
+				<option value="between" data-values="2">Entre</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" class="form-control filter-value">
+		</div>
+		<div class="filter-value-block" data-value-type="dictionnary" data-value-selector=".filter-value:last-child">
+			<select class="form-control filter-operator border-0 text-primary">
+				<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-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">
+				<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>
 
 	<!-- 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>
+		<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">
+			<span class="dropdown-item">
+		  		<input type="text" placeholder="Mot clé" class="form-control w-100">
+			</span>
+			<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>
 	</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">
+				<div class="modal-header bg-info text-light">
 					<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>
@@ -187,26 +218,28 @@ $cacheVersion = isset($cacheVersion) ? $cacheVersion : 1;
 	<?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>
-
+	<!-- lib js -->
 	<script src="<?php echo $mediaRoot ?>/js/vendor/popper.min.js"></script>
-	<script src="<?php echo $mediaRoot ?>/js/bootstrap.min.js"></script>
+	
 	<script src="<?php echo $mediaRoot ?>/js/vendor/mustache.min.js"></script>
 	<script src="<?php echo $mediaRoot ?>/js/vendor/jquery-ui.min.js"></script>
 	<script src="<?php echo $mediaRoot ?>/js/vendor/jquery.timepicker.js"></script>
+	<!-- prevent jquery ui / boostrap conflit on $.tooltip() -->
+	<script>$.widget.bridge('uitooltip', $.ui.tooltip);</script>
+	<script src="<?php echo $mediaRoot ?>/js/vendor/bootstrap.min.js"></script>
 	<script src="<?php echo $mediaRoot ?>/js/vendor/bootstrap3-typeahead.min.js"></script>
 	<script src="<?php echo $mediaRoot ?>/js/vendor/trumbowyg.min.js"></script>
-	<script src="<?php echo $mediaRoot ?>/js/vendor/trumbowyg.colors.js"></script>
-	<script src="<?php echo $mediaRoot ?>/js/vendor/trumbowyg.table.js"></script>
+	<script src="<?php echo $mediaRoot ?>/js/vendor/trumbowyg.plugins.js"></script>
+	<!-- custom js -->
 	<script src="<?php echo $mediaRoot ?>/js/fontawesome.js"></script>
 	<script src="<?php echo $mediaRoot ?>/js/vendor/Chart.min.js"></script>
 	<script src="<?php echo $mediaRoot ?>/js/plugins.js?v=<?php echo $cacheVersion ?>"></script>
+	<script src="<?php echo $mediaRoot ?>/js/filter.component.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>
-</html>
+</html>

+ 430 - 122
function.php

@@ -32,7 +32,7 @@ function unhandledException($ex){
 	    break;
 	}
 	
-	require_once('footer.php');
+	require_once(__DIR__.SLASH.'footer.php');
 	exit();
 }
 
@@ -40,6 +40,28 @@ function get_OS(){
 	return strtoupper(substr(PHP_OS, 0, 3));
 }
 
+function OS_path_max_length(){
+	switch(get_OS()){
+		case 'WIN':
+			return 259;
+		break;
+		default:
+			return 4096;
+		break;
+	}
+}
+
+function OS_element_max_length(){
+	switch(get_OS()){
+		case 'WIN':
+			return 255;
+		break;
+		default:
+			return 255;
+		break;
+	}
+}
+
 function ip(){
 	if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
 		$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
@@ -115,6 +137,25 @@ if(!function_exists('glob_recursive')) {
 	}
 }
 
+//Récupère la dernière clé d'un tableau
+//avec fallback pour versions de PHP < 7
+if (!function_exists("array_key_last")) {
+    function array_key_last($array) {
+        if (!is_array($array) || empty($array)) return NULL;
+        return array_keys($array)[count($array)-1];
+    }
+}
+
+//Récupère la première clé d'un tableau
+//avec fallback pour versions de PHP < 7
+if(!function_exists('array_key_first')) {
+    function array_key_first($array) {
+        foreach($array as $key => $unused) 
+            return $key;
+        return NULL;
+    }
+}
+
 function core_reference(){
 	$token = 'az9e87qS65d4A32f1d65df4s8d5d2cc';
 	$constant = __ROOT__.'constant.php';
@@ -187,6 +228,28 @@ function array_map_recursive($callback, $array) {
 	return array_map($func, $array);
 }
 
+//Même principe que le ORDER BY de MySQL
+//mais sur différentes clés d'un tableau
+//Auteur: jimpoz -> https://www.php.net/manual/fr/function.array-multisort.php#100534
+function array_orderby() {
+	//Permet de récupérer les paramètres passés dynamiquement
+	//Car on ne sait pas combien de paramètres on peut avoir
+    $args = func_get_args();
+    $data = array_shift($args);
+
+    foreach ($args as $n => $field) {
+        if (is_string($field)) {
+            $tmp = array();
+            foreach ($data as $key => $row)
+                $tmp[$key] = $row[$field];
+            $args[$n] = $tmp;
+        }
+    }
+    $args[] = &$data;
+    call_user_func_array('array_multisort', $args);
+    return array_pop($args);
+}
+
 function secure_user_vars($var){
 	if(is_array($var)){
 		$array = array();
@@ -223,7 +286,7 @@ function getExtIcon($ext){
 		case 'rar':
 		case 'gz':
 		case 'zip':
-		$icon = 'far fa-file-archive text-warning';
+			$icon = 'far fa-file-archive text-warning';
 		break;
 		
 		case 'php':
@@ -238,43 +301,44 @@ function getExtIcon($ext){
 		case 'htm':
 		case 'asp':
 		case 'jsp':
-		$icon = 'fas fa-file-code text-secondary text-warning';
+			$icon = 'fas fa-file-code text-secondary text-warning';
 		break;
 		
 		case 'xls':
 		case 'xlsx':
 		case 'csv':
-		$icon = 'far fa-file-excel text-success';
+			$icon = 'far fa-file-excel text-success';
 		break;
 		
 		case 'bmp':
 		case 'jpg':
+		case 'jfif':
 		case 'jpeg':
 		case 'ico':
 		case 'gif':
 		case 'png':
 		case 'svg':
-		$icon = 'far fa-file-image text-info';
+			$icon = 'far fa-file-image text-info';
 		break;
 		
 		case 'pdf':
-		$icon = 'far fa-file-pdf text-danger';
+			$icon = 'far fa-file-pdf text-danger';
 		break;
 		case 'ppt':
 		case 'pptx':
-		$icon = 'fa-file-powerpoint-o text-warning' ;
+			$icon = 'fa-file-powerpoint-o text-warning' ;
 		break;
 		
 		case 'txt':
 		case 'htaccess':
 		case 'md':
-		$icon = 'far fa-file-alt';
+			$icon = 'far fa-file-alt';
 		break;
 		
 		case 'doc':
 		case 'docx':
 		case 'word':
-		$icon = 'far fa-file-word text-primary';
+			$icon = 'far fa-file-word text-primary';
 		break;
 		
 		case 'avi':
@@ -288,7 +352,7 @@ function getExtIcon($ext){
 		case 'h264':
 		case 'rmvb':
 		case 'mp4':
-		$icon = 'far fa-file-movie text-secondary';
+			$icon = 'far fa-file-video text-secondary';
 		break;
 		
 		case 'wav':
@@ -302,10 +366,10 @@ function getExtIcon($ext){
 		case 'flac':
 		case 'aac':
 		case 'mp3':
-		$icon = 'far fa-file-audio text-secondary';
+			$icon = 'far fa-file-audio text-secondary';
 		break;
 		default:
-		$icon = 'far fa-file';
+			$icon = 'far fa-file';
 		break;
 	}
 	return $icon;
@@ -351,70 +415,77 @@ function getExtContentType($ext){
 		// break;
 		
 		case '7z':
-		$cType = 'application/x-7z-compressed';
+			$cType = 'application/x-7z-compressed';
 		break;
 		case 'rar':
-		$cType = 'application/x-rar-compressed';
+			$cType = 'application/x-rar-compressed';
 		break;
 		case 'gz':
-		$cType = 'application/x-gzip';
+			$cType = 'application/x-gzip';
 		break;
 		case 'zip':
-		$cType = 'application/zip';
+			$cType = 'application/zip';
 		break;
 		case 'xls':
-		$cType = 'application/vnd.ms-excel';
+			$cType = 'application/vnd.ms-excel';
 		break;
 		case 'xlsx':
-		$cType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
+			$cType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
 		break;
 		case 'csv':
-		$cType = 'text/csv';
+			$cType = 'text/csv';
 		break;
 
 		case 'jpg':
 		case 'jpeg':
-		$cType = 'image/jpeg';
+			$cType = 'image/jpeg';
 		break;
 		case 'bmp':
 		case 'gif':
 		case 'png':
-		$cType = 'image/'.$ext;
+			$cType = 'image/'.$ext;
 		break;
 
 		case 'ppt':
-		$cType = 'application/vnd.ms-powerpoint';
+			$cType = 'application/vnd.ms-powerpoint';
 		break;
 		case 'pptx':
-		$cType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
+			$cType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
 		break;
 		case 'pdf':
-		$cType = 'application/pdf';
+			$cType = 'application/pdf';
 		break;
 		case 'txt':
-		$cType = 'text/plain';
+			$cType = 'text/plain';
 		break;
 		
 		case 'doc':
 		case 'word':
-		$cType = 'application/msword';
+			$cType = 'application/msword';
 		break;
 		case 'docx':
-		$cType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
+			$cType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
 		break;
 		
 		case 'aac':
 		case 'wav':
-		$cType = 'audio/'.$ext;
+			$cType = 'audio/'.$ext;
 		break;
-		$cType = 'audio/aac';
+			$cType = 'audio/aac';
 		break;
 		case 'mp3':
-		$cType = 'audio/mpeg3';
+			$cType = 'audio/mpeg3';
+		break;
+
+		case 'otf':
+		case 'ttf':
+		case 'woff':
+		case 'woff2':
+			$cType = 'font/'.$ext;
 		break;
 
 		default:
-		$cType = 'application/octet-stream';
+			$cType = 'application/octet-stream';
 		break;
 	}
 	return $cType;
@@ -446,7 +517,7 @@ function relative_time($date,$date2 = null, $relativeLimit = null, $detailled =
     if(isset($date2)) $to->setTimestamp($date2);
     $intervalle = $from->diff($to);
 
-    if(isset($relativeLimit) && $intervalle->format('%d') > $relativeLimit){
+    if(isset($relativeLimit) && $intervalle->days > $relativeLimit){
     	$limitFormat = $detailled ? 'd/m/Y - H:i' : 'd/m/Y';
     	return date($limitFormat, $date);
     }
@@ -528,57 +599,191 @@ function format_date_diff($date1, $date2, $format='%a'){
 	return $interval->format($format);
 }
 
-// Construit une requete sécurisée pour le composant filtre
-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','not like','in','not in','between','is null','is not null'))) return;
-		if(!in_array(strtolower($filter['join']), array('and','or'))) return;
-
-		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){
-					$filter['value'][$i] = timestamp_date($value);
-				}
-			} else {
-				$filter['value'] = timestamp_date($filter['value']);
+/**
+ * Normalise un tableau de filtres pour le composant Filter
+ * 
+ * $filters : Array => tableau des paramètres avancés au format
+ * array(
+ *   "jean",	//Mot clé de la recherche simple
+ * 	 array(
+ *    	'nom_de_votre_colonne:operateur' 	=> 'valeur_attendue',
+ *    	'join' 	=> 'and|or', //Facultatif
+ *   ),
+ *   etc... (à répéter pour les N critères)
+ * )
+ * Eg. :
+ *  filters_default(array(
+ *     "jean",
+ *     array(
+ *         'birth' => "17/09/1998",
+ *         'join' => 'or'
+ *     ),
+ *     array(
+ *         'phone:like' => "9754"
+ *     )
+ *  ));
+ */
+function filters_default($filters){
+	$finalFilters = array();
+
+	foreach ($filters as $filter) {
+		//Gestion du keyword
+		if(!is_array($filter) && !is_object($filter)){
+			array_unshift($finalFilters, $filter);
+			continue;
+		}
+
+
+		$tempFilter = array();
+		foreach ($filter as $column => $value) {
+			//Gestion du join
+			if ($column == 'join') {
+				$tempFilter['join'] = $value;
+				continue;
 			}
+			//Gestion de la colonne ciblée avec opérator
+			$column = explode(':', $column);
+			$tempFilter['column'] = $column[0];
+			if(isset($column[1])) $tempFilter['operator'] = $column[1];
+
+			//Gestion de la value
+			$tempFilter['value'] = $value;
 		}
+		if(!empty($tempFilter)) $finalFilters[] = $tempFilter;
+	}
+
+	return filters_set($finalFilters);
+	
+}
+
+/**
+ * Normalise et contrôles sur un tableau
+ * de filtres pour le composant Filter.
+ * 
+ * $filters : Array => tableau des paramètres avancés au format
+ * array(
+ *   "jean",	//Mot clé de la recherche simple, peut aussi être nommée 'keyword' => "jean"
+ * 	 array(
+ *    	'column' 	=> 'nom_de_votre_colonne]',
+ *     	'operator' 	=> '=|!=|LIKE:<:>', 					//Facultatif => "=" par défaut
+ *      'value' 	=> 'valeur_attendue',					//Peut être du type array si plusieurs valeurs attendues
+ *      'join' 		=> 'and|or'								//Facultatif => "and" par défaut
+ *   ),
+ *   etc... (à répéter pour les N critères)
+ * )
+ * Eg. :
+ * filters_set(array(
+ * 	 'test',
+ *   array(
+ *   	'column' => 'login',
+ *   	'value'	 => 'admin'
+ *   )
+ * ));
+ */
+function filters_set($filters){
+	$finalFilters = array(
+		'k' => '',
+		'a' => array()
+	);
+
+	foreach ($filters as $key => $filter) {
+
+		if($key==="keyword" || (!is_array($filter)) ) {
+			$finalFilters['k'] = $filter;
+			continue;
+		}
+		if(!isset($filter['column']) || !isset($filter['value'])) return;
+
+		$filter['operator'] = isset($filter['operator']) ? $filter['operator'] : '=';
+		if(!in_array(strtolower($filter['operator']), array('<','>','=','!=','like','not like','between','is null','is not null'))) return;
+
+		$tempFilter = array(
+			"c" => $filter['column'], 
+			"o" => $filter['operator'], 
+			"v" => is_array($filter['value']) ? $filter['value'] : array($filter['value']), 
+		);
 		
-		switch(strtolower($filter['operator'])){
-			case 'like':
-			case 'not like':
-				$query .= ' '.$filter['join'].' '.$filter['column'].' '.$filter['operator'].' ?';
-				$data[] =  '%'.$filter['value'].'%' ;
-			break;
-			case 'is null':
-				$query .= ' '.$filter['join'].' ('.$filter['column'].' IS NULL OR '.$filter['column'].'="") ';
-			break;
-			case 'is not null':
-				$query .= ' '.$filter['join'].' ('.$filter['column'].' IS NOT NULL AND '.$filter['column'].'!="") ';
-			break;
-			case 'between':
+		if(isset($filter['join'])){
+			if(!in_array(strtolower($filter['join']), array('and','or'))) return;
+			$tempFilter['j'] = $filter['join'];
+		}
+		if(isset($filter['type'])) $tempFilter['t'] = $filter['type'];
+		$finalFilters['a'][] = $tempFilter;
+	}
+
+	return $finalFilters;
+}
+
+// Construit une requete sécurisée pour le composant filtre
+function filter_secure_query($filters,$allowedColumns,&$query,&$data,$iteration = 0,$columnEscape="`"){
+	if($iteration==0 && !empty($filters)) $query .= ' AND (';
+	
+	foreach ($filters as $i=>$filter) {
+		if(isset($filter['group'])){
+			$query .= ' ( ';
+			 filter_secure_query($filter['group'],$allowedColumns,$query,$data,$iteration+1);
+			$query .= ' ) ';
+		}else{
+			$filter['operator'] = html_entity_decode($filter['operator']);
+			if(!in_array($filter['column'], $allowedColumns)) throw new Exception("Colonne '".$filter['column']."' interdite", 400);
+			if(!in_array(strtolower($filter['operator']), array('<','>','=','!=','like','not like','in','not in','between','is null','is not null'))) return;
+			if(isset($filter['join']) && !in_array(strtolower($filter['join']), array('and','or'))) return;
+			if(!preg_match("/.*\..*/i", $filter['column']))
+				$filter['column'] = $columnEscape.$filter['column'].$columnEscape;
+			
+			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'])){
-					$query .= ' '.$filter['join'].' '.$filter['column'].' '.$filter['operator'].' ? AND ?';
-					$data[] =  $filter['value'][0] ;
-					$data[] =  $filter['value'][1];
+					foreach($filter['value'] as $j=>$value){
+						$filter['value'][$j] = timestamp_date($value);
+					}
+				} else {
+					$filter['value'] = timestamp_date($filter['value']);
 				}
-			break;
-			default:
-				if(is_array($filter['value']))
-					$filter['value'] = array_pop($filter['value']);
-				
-				$query .= ' '.$filter['join'].' '.$filter['column'].' '.$filter['operator'].' ?';
-				$data[] = $filter['value'];
-			break;
+			}
+
+			switch(strtolower($filter['operator'])){
+				case 'like':
+				case 'not like':
+					if(is_array($filter['value'])) $filter['value'] = array_pop($filter['value']);
+					//$query .= $i==0 ? ' '.$filter['join'].' (' : ' '.$filter['join'].' ';
+					$query .= ' '.$filter['column'].' '.$filter['operator'].' ?';
+					$data[] =  '%'.$filter['value'].'%' ;
+				break;
+				case 'is null':
+					//$query .= $i==0 ? ' '.$filter['join'].' (' : ' '.$filter['join'].' ';
+				    $query .= ' ('.$filter['column'].' IS NULL OR '.$filter['column'].'="") ';
+				break;
+				case 'is not null':
+					//$query .= $i==0 ? ' '.$filter['join'].' (' : ' '.$filter['join'].' ';
+				    $query .= ' ('.$filter['column'].' IS NOT NULL AND '.$filter['column'].'!="") ';
+				break;
+				case 'between':
+					if(is_array($filter['value'])){
+						//$query .= $i==0 ? ' '.$filter['join'].' (' : ' '.$filter['join'].' ';
+						$query .= ' '.$filter['column'].' '.$filter['operator'].' ? AND ?';
+						$data[] =  $filter['value'][0];
+						$data[] =  $filter['value'][1];
+					}
+				break;
+				case 'in':
+				    $query .= ' ('.$filter['column'].' IN (?) ) ';
+				    $data[] =  implode(',',$filter['value']);
+				break;
+				default:
+					if(is_array($filter['value']))
+						$filter['value'] = array_pop($filter['value']);
+					
+					//$query .= $i==0 ? ' '.$filter['join'].' (' : ' '.$filter['join'].' ';
+					$query .= ' '.$filter['column'].' '.$filter['operator'].' ?';
+					$data[] = $filter['value'];
+				break;
+			}
 		}
+		if(isset($filter['join'])) $query .= ' '.$filter['join'].' ';
 	}
+	if($iteration==0 && !empty($filters)) $query .= ')';
 }
 
 function sort_secure_query($sort,$allowedColumns,&$query){
@@ -717,7 +922,7 @@ function mb_ucfirst($string,$encoding = 'UTF-8'){
 // Permet de mettre au bon format le n° de 
 // téléphone fourni dans le formulaire
 function normalize_phone_number($number){
-	$nb = str_replace(array(' ', '.', ','), '', $number);
+	$nb = str_replace(array(' ', '.', ',', '-'), '', $number);
 	$nb = chunk_split($nb, 2, ' ');
 	$nb = rtrim($nb);
 	preg_match("/^[0-9]{2} [0-9]{2} [0-9]{2} [0-9]{2} [0-9]{2}/", $nb, $matches);
@@ -726,8 +931,10 @@ function normalize_phone_number($number){
 
 // Permet de voir si le format du n° de téléphone
 // fourni correspond à un format correct
-function check_phone_number($number){
-	$nb = str_replace(array(' ', '.', ','), '', $number);
+function check_phone_number($number, $international=false){
+	if(!$international && preg_match("/\+/i", $number)) return false;
+
+	$nb = str_replace(array(' ', '.', ',', '-'), '', $number);
 	if (!is_numeric($nb)) return false;
 	return true;
 }
@@ -813,11 +1020,9 @@ function find_best_match($words = array(), $input = '') {
 }
 // Convertit nombres en lettres (utile pour Excel)
 function numbers_to_letters($num){
-
 	$num = intval($num);
-	if ($num <= 0) return '';
-
 	$letter = '';
+	if ($num <= 0) return $letter;
 
 	while($num != 0){
 		$p = ($num - 1) % 26;
@@ -826,29 +1031,17 @@ function numbers_to_letters($num){
 	}
 	return $letter;     
 }
-// Convertit nombres en lettres (utile pour Excel)
-// eg: 1==A
-function numbers_to_letters_2($num) {
-	$numeric = ($num - 1) % 26;
-	$letter = chr(65 + $numeric);
-	$num2 = intval(($num - 1) / 26);
-	if ($num2 > 0) {
-		return getNameFromNumber($num2) . $letter;
-	} else {
-		return $letter;
-	}
-}
 //Convertit lettres en nombres (utile pour Excel)
 function letters_to_numbers($col){
 	$col = str_pad($col,3,'0', STR_PAD_LEFT);
 	$i = 0;
-	if ($col{0} != '0') {
-		$i = ((ord($col{0}) - 64) * 676) + 26;
-		$i += ($col{1} == '0') ? 0 : (ord($col{1}) - 65) * 26;
+	if ($col[0] != '0') {
+		$i = ((ord($col[0]) - 64) * 676) + 26;
+		$i += ($col[1] == '0') ? 0 : (ord($col[1]) - 65) * 26;
 	} else {
-		$i += ($col{1} == '0') ? 0 : (ord($col{1}) - 64) * 26;
+		$i += ($col[1] == '0') ? 0 : (ord($col[1]) - 64) * 26;
 	}
-	$i += ord($col{2}) - 64;
+	$i += ord($col[2]) - 64;
 	return $i;
 }
 //Check si c'est un date bien formattée
@@ -885,8 +1078,8 @@ function delete_folder_tree($dir, $selfDestroy=false) {
 }
 
 //Normalise les caractères un peu spéciaux
-//d'une chaîne de calculhmac(ractère, data)
-function normalize_chars($string) {
+//d'une chaîne de caractère
+function normalize_chars($string, $mask=array()) {
 	$normalizeChars = array(
 		'Š'=>'S', 'š'=>'s', 'Ð'=>'Dj','Ž'=>'Z', 'ž'=>'z', 'À'=>'A', 'Á'=>'A', 'Â'=>'A', 'Ã'=>'A', 'Ä'=>'A',
 		'Å'=>'A', 'Æ'=>'A', 'Ç'=>'C', 'È'=>'E', 'É'=>'E', 'Ê'=>'E', 'Ë'=>'E', 'Ì'=>'I', 'Í'=>'I', 'Î'=>'I',
@@ -896,8 +1089,9 @@ function normalize_chars($string) {
 		'î'=>'i', 'ï'=>'i', 'ð'=>'o', 'ñ'=>'n', 'ń'=>'n', 'ò'=>'o', 'ó'=>'o', 'ô'=>'o', 'õ'=>'o', 'ö'=>'o',
 		'ø'=>'o', 'ù'=>'u', 'ú'=>'u', 'û'=>'u', 'ü'=>'u', 'ý'=>'y', 'ý'=>'y', 'þ'=>'b', 'ÿ'=>'y', 'ƒ'=>'f',
 		'ă'=>'a', 'î'=>'i', 'â'=>'a', 'ș'=>'s', 'ț'=>'t', 'Ă'=>'A', 'Î'=>'I', 'Â'=>'A', 'Ș'=>'S', 'Ț'=>'T',
+		'’'=>'\'', '–'=>'-', '€'=>'&euro;', '&'=>'&amp;','œ'=>'oe', '•'=>'-'
 	);
-	return strtr($string, $normalizeChars);
+	return strtr($string, array_diff_key($normalizeChars, array_flip($mask)));
 }
 
 //Convertit un nombre en son équivalent écrit
@@ -1046,6 +1240,28 @@ function number_to_ordinal($number, $feminine=false){
 	return $number.'ème';
 }
 
+/**
+ * Vérifie si un nombre est entre 2 bornes
+ * Possibilité d'inclure ou d'exclure les bornes avec $strict
+ */
+function number_between($number, $low, $high, $strict=false) {
+	if(!$strict && ($number<$low || $number>$high)) return false;
+	if($strict && ($number<=$low || $number>=$high)) return false;
+	return true;
+}
+
+/*
+ * Fonction pour vérifier si un nombre
+ * est un nombre décimal
+ *
+ * Eg: 	- 95.00 --> false
+ * 		- 95.5  --> true
+ * Les nombres avec décimales à 0 sont donc exclus
+ */
+function is_decimal($val) {
+    return is_numeric($val) && floor($val)!=$val;
+}
+
 /**
  * Retourne les fonction interdites
  * utilisées dans une fonction eval()
@@ -1106,28 +1322,29 @@ function forbidden_macro($source){
 	return $forbiddens;
 }
 
-function template($stream,$data){
+function template($stream,$data,$mustacheTemplate = false){
    //loop
-    $stream = preg_replace_callback('/{{\:([^\/\:\?}]*)}}(.*?){{\/\:[^\/\:\?}]*}}/',function($matches) use ($data) {
+    $stream = preg_replace_callback('/{{\:([^\/\:\?}]*)}}(.*?){{\/\:[^\/\:\?}]*}}/is',function($matches) use ($data) {
         $tag = $matches[1];
         $streamTpl = $matches[2];
 
         $stream = '';
         if(!isset($data[$tag])) return $stream;
-            $i = 0;
-            $values = $data[$tag];
-            foreach($values as $join){
-                $occurence = $streamTpl;
-                foreach($join as $key=>$value){
-                    $occurence = str_replace(array('{{'.$key.'}}'),array($value),$occurence); 
-                }
-                $stream.= $occurence;
+        $values = $data[$tag];
+
+        foreach($values as $join){
+            $occurence = $streamTpl;
+            foreach($join as $key=>$value){
+            	if(is_array($value) || is_object($value)) continue;
+                $occurence = str_replace(array('{{'.$key.'}}'),array($value),$occurence);
             }
-            return $stream;
-    },$stream); 
+            $stream .= $occurence;
+        }
+        return $stream;
+    },$stream);
 
     //conditions
-    $stream = preg_replace_callback('/{{\?([^\/\:\?}]*)}}(.*?){{\/\?[^\/\:\?}]*}}/',function($matches) use ($data) {
+    $stream = preg_replace_callback('/{{\?([^\/\:\?}]*)}}(.*?){{\/\?[^\/\:\?}]*}}/is',function($matches) use ($data) {
         $key = $matches[1];
         $stream = $matches[2];
         return !isset($data[$key]) || (is_array($data[$key]) && count($data[$key])==0) ?'':$stream;
@@ -1136,8 +1353,8 @@ function template($stream,$data){
     //simple vars
     $stream = preg_replace_callback('/{{([^\/\:\;\?}]*)}}/',function($matches) use ($data) {
         $key = $matches[1];
-        
-        if (isset($data[$key]) && is_string($data[$key])) return $data[$key];
+
+        if(isset($data[$key]) && (is_string($data[$key]) || is_numeric($data[$key])) ) return $data[$key];
 
         $value = '';
         $attributes = explode('.',$key);
@@ -1147,13 +1364,87 @@ function template($stream,$data){
         	$current = $current[$attribute];
         	$value = $current;
         }
-
-        return is_string($value) ? $value : '';
+        return $value;
     },$stream); 
   
     return $stream;
 }
 
+/**
+ * Auteur: Anonymous
+ * Lien: https://www.php.net/manual/en/features.file-upload.post-method.php#120686
+ * 
+ * Normalise le tableau de fichier récupérés
+ * pour une utilisation plus efficace et intuitive
+ * 
+ * Exemple de format initial pour 2 fichiers dans $_FILES :
+ * Array (
+    [name] => Array (
+        [0] => foo.txt
+        [1] => bar.txt
+    ),
+    [type] => Array(
+        [0] => text/plain
+        [1] => text/plain
+    ),
+    [tmp_name] => Array(
+        [0] => /tmp/phpYzdqkD
+        [1] => /tmp/phpeEwEWG
+    ),
+    [error] => Array(
+        [0] => 0
+        [1] => 0
+    ),
+    [size] => Array(
+        [0] => 123
+        [1] => 456
+    )
+ * )
+ * 
+ * Exemple de format de retour pour 2 fichiers :
+ * Array(
+    [0] => Array(
+        [name] => foo.txt
+        [type] => text/plain
+        [tmp_name] => /tmp/phpYzdqkD
+        [error] => 0
+        [size] => 123
+    ),
+    [1] => Array(
+        [name] => bar.txt
+        [type] => text/plain
+        [tmp_name] => /tmp/phpeEwEWG
+        [error] => 0
+        [size] => 456
+    )
+)
+ * @param  Array &$file_post [tableau de fichiers $_FILES]
+ * @return Array             [le tableau ré-arrangé]
+ */
+function normalize_php_files() {
+    $function = function($files, $fixedFiles=array(), $path=array()) use (&$function) {
+        foreach ($files as $key => $value) {
+            $temp = $path;
+            $temp[] = $key;
+       
+            if(is_array($value)) {
+                $fixedFiles = $function($value, $fixedFiles, $temp);
+            } else {
+                $next = array_splice($temp, 1, 1);
+                $temp = array_merge($temp, $next);
+                $new = &$fixedFiles;
+               
+                foreach ($temp as $key)
+                    $new = &$new[$key];
+
+                $new = $value;
+            }
+        }
+        return $fixedFiles;
+    };
+    return $function($_FILES);
+}
+
 /**
  * Incrémente automatiquement le nom d'un
  * fichier si celui-ci existe déjà
@@ -1233,6 +1524,12 @@ function day_name($day){
     return isset($translates[$day-1])? $translates[$day-1]:$day;
 }
 
+//Convertit un timestamp dans un format 
+//agréable et complet : ex Mardi 18 Avril 2019
+function complete_date($time=null){
+	if(!isset($time)) $time = time();
+	return day_name(date('N',$time)).' '.date('d',$time).' '.month_name(date('m',$time)).' '.date('Y',$time);
+}
 
 //Récuperation des jours féries
 function get_not_workable($date=null){
@@ -1288,9 +1585,9 @@ function is_array_unique($array=array()){
 function make_cookie($nom, $valeur, $expire='',$ns ='/') {
 	if($expire == ''){
 		setcookie($nom, $valeur, mktime(0,0,0, date("d"), date("m"), (date("Y")+1)), $ns);
-		} else {
+	} else {
 		setcookie($nom, '', mktime(0,0,0, date("d"), date("m"), (date("Y")-1)), $ns);
-		}
+	}
 }
 //Permet de formatter les prix à afficher
 //de la même manière partout sur l'ERP.
@@ -1355,12 +1652,23 @@ if( !function_exists('apache_request_headers') ) {
 }
 
 
-//Effectue un basename en tenant compte des caractères utf8( non pris en compte par basename PHP sous linux)
+//Effectue un basename en tenant compte des caractères
+//utf8( non pris en compte par basename PHP sous linux)
 function mt_basename($path){
 	$path = str_replace(array("\\","/"),SLASH, $path);
 	$nameSplit = explode(SLASH,$path);
 	return end($nameSplit);
 }
 
+//filtre les balises dangeureuses (script,link..) dans les contenus wysiwyg 
+function wysiwyg_filter($html){
+	
+	$html = preg_replace('#<(script|link)(.*?)>(.*?)</(script|link)>#is', '', $html);
+	$html = preg_replace('#<(script|link)([^>]*?)>#is', '', $html);
+	return $html;
+}
 
-?>
+function display_format_price($price) {
+	return display_price(format_price($price));
+}
+?>

+ 71 - 71
header.php

@@ -3,14 +3,12 @@
 $scheme = define_url_scheme();
 $mediaRoot = define_media_root();
 
-
 if($myUser->preference('passwordTime')!='' && !$myUser->superadmin){
 	$basepage = explode('?',$page);
 	$basepage = $basepage[0];
 	if(is_numeric($conf->get('password_delay')) && $basepage!="account.php" && ( (time() - ($conf->get('password_delay') * 86400) > $myUser->preference('passwordTime'))  )  ) header('location: account.php?error=Votre mot de passe est trop vieux, veuillez le renouveller ci dessous.');
 }
 
-
 //Redirige la home si définie dans les settings
 if(($page=='index.php' || $page==basename(ROOT_URL) || $page=='') && !isset($_['module']) && !empty($conf->get('home_page')))
 	header('location:'.$conf->get('home_page'));
@@ -42,7 +40,7 @@ if(!isset($_['admin_login']) && file_exists('enabled.maintenance') && !$myUser->
 		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 		<meta name="description" content="">
 		<meta name="author" content="">
-		<!-- <link rel="shortcut icon" type="image/x-icon" href="img/favicon.ico"> -->
+		<link rel="shortcut icon" type="image/x-icon" href="img/favicon.ico">
     	<link rel="shortcut icon" type="image/png" href="action.php?action=general_favicon_download" />
 	
 		<?php 
@@ -70,8 +68,8 @@ if(!isset($_['admin_login']) && file_exists('enabled.maintenance') && !$myUser->
 		<link href="<?php echo $mediaRoot ?>/css/trumbowyg.colors.css" rel="stylesheet">
 
 		<?php if(!$conf->get('offline_mode')): ?>
-			<!-- Lato font-->
-			<link href="https://fonts.googleapis.com/css?family=Lato:300,400" rel="stylesheet">
+		<!-- Lato font-->
+		<link href="https://fonts.googleapis.com/css?family=Lato:300,400" rel="stylesheet">
 		<?php endif; ?>
 
 		<!-- Custom styles for this template -->
@@ -83,7 +81,7 @@ if(!isset($_['admin_login']) && file_exists('enabled.maintenance') && !$myUser->
 	<body>
 		<!-- Fixed navbar -->
 		<nav id="mainMenu" class="navbar navbar-expand-md navbar-dark fixed-top noPrint">
-			<a class="navbar-brand" style="background-image: url('action.php?action=general_logo_download')" href="index.php"><?php echo $conf->get('show_application_name') ? PROGRAM_NAME : ''; ?></a>
+			<?php if ($myUser->connected()): ?>
 			<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
 				<div class="menu">
 					<div class="menu-icon">
@@ -95,82 +93,84 @@ if(!isset($_['admin_login']) && file_exists('enabled.maintenance') && !$myUser->
 					</div>
 				</div>
 			</button>
+			<?php endif; ?>
+			<a class="navbar-brand" style="background-image: url('action.php?action=general_logo_download')" <?php echo $conf->get('logo_website_header') ? 'target="_blank" href="'.$conf->get('logo_website_header').'"' : 'href="index.php"'; ?>><?php echo $conf->get('show_application_name') ? PROGRAM_NAME : ''; ?></a>
 			<div class="collapse navbar-collapse" id="navbarCollapse">
 				<ul class="navbar-nav navbar-main">
-					<?php foreach($mainMenu 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'])
-							|| (isset($item['active']) && $item['active'] ==true)
-						){
-							$classes .= ' active';
-						} 
-						?>
-						<li data-id="<?php echo isset($item['id'])?$item['id']:''; ?>"
-							class="nav-item <?php echo $classes; ?> ">
-							
-							<a class="nav-link" 
-								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>
-						</li>
-					<?php endforeach; ?>
+				<?php foreach($mainMenu 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'])
+						|| (isset($item['active']) && $item['active'] ==true)
+					){
+						$classes .= ' active';
+					} 
+					?>
+					<li data-id="<?php echo isset($item['id'])?$item['id']:''; ?>" class="nav-item <?php echo $classes; ?> ">
+						<a class="nav-link" 
+							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>
+					</li>
+				<?php endforeach; ?>
 				</ul>
 				<?php Plugin::callHook("header", array()); ?>
-				<div id="loginHeader" class="ml-auto text-right">
-					
-					<?php Plugin::callHook("login_header", array()); ?>
-					<div class="dropdown user-dropdown-menu">
-						<button class="btn btn-dark dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-							<?php if ($myUser->connected()): ?>
-								<img src="<?php echo $myUser->getAvatar(); ?>" class="avatar-mini avatar-rounded avatar-login" title="<?php echo $myUser->fullName(); ?>">
-							<?php else: ?>
-							Non connecté - Connexion
-							<?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" data-action="login" class="form-inline mt-mb-0">
-									<label for="login">Identifiant</label>
-									<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 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>
-									</div>
-									<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 id="loginHeader" class="ml-auto d-flex text-right">
+				<?php Plugin::callHook("login_header", array());					
+					if(!$conf->get('hide_header_login') || $myUser->connected()): ?>
+						<div class="dropdown user-dropdown-menu ml-1">
+							<button class="btn btn-dark dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+								<?php if ($myUser->connected()): ?>
+									<img src="<?php echo $myUser->getAvatar(); ?>" class="avatar-mini avatar-rounded avatar-login" title="<?php echo htmlentities($myUser->fullName()); ?>">
+								<?php else: ?>
+									Connexion
+								<?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" data-action="login" class="form-inline mt-mb-0 login-form">
+										<label for="login">Identifiant</label>
+										<input name="login" id="login" maxlength="260" class="form-control form-control-sm  mb-2" type="text" required="required" autofocus="true" autocomplete="username">
+										<label for="password">Mot de passe</label> 
+										<input data-type="password" name="password" id="password" class="form-control form-control-sm " type="password" required="required" autocomplete="current-password">
+										<div class="form-check connection-remember">
+										    <input data-type="checkbox" class="form-check-input" type="checkbox" id="rememberMe" name="rememberMe">
+										    <label class="form-check-label" for="rememberMe">Se souvenir de moi</label>
+										</div>
+										<div class="btn btn-success btn-sm w-100" id="login-button" onclick="core_login(this,'body>.container-fluid')" tabindex="0">Se connecter</div>
+										<?php if($conf->get('password_allow_lost')): ?>
+										<a href="account.lost.php" title="Changer son mot de passe oublié" class="d-block w-100 text-center mt-1 lost-password">Mot de passe oublié ?</a>
+										<?php endif; ?>
+									</form>
+								</div>
+								<?php else: ?>
 								<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'];
+								<?php foreach($userMenu as $item): ?>
+									<?php if(isset($item['custom'])):
+										echo $item['custom'];
 									else: ?>
-									<a class="dropdown-item" href="<?php echo $item['url']; ?>">
-										<?php echo (isset($item['icon'])?'<i class="'.$item['icon'].'"></i> ':'').$item['label']; ?>
-									</a>
+										<a class="dropdown-item user-menu-item" href="<?php echo $item['url']; ?>">
+											<?php echo (isset($item['icon'])?'<i class="'.$item['icon'].'"></i> ':'').$item['label']; ?>
+										</a>
+									<?php endif; ?>
+								<?php endforeach; ?>
 								<?php endif; ?>
-							<?php endforeach; ?>
-							<?php endif; ?>
+							</div>
 						</div>
-					</div>
+					<?php endif; ?>
 				</div>
 			</div>
 		</nav>
 	
 		<!-- Begin page content -->
 		<div class="container-fluid">
-<?php } ?>
+<?php } ?>

+ 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


+ 231 - 112
install.php

@@ -1,6 +1,43 @@
 <?php
 
+if(isset($_GET['action']) && $_GET['action'] =='check_connection'){
+	$response = array();
+
+	require_once(__DIR__.'/connector/'.$_GET['connector'].'.class.php');
+	$connector = $_GET['connector'];
+	$connectionString = $connector::connection;
+    foreach ($_GET as $key => $value) {
+         $connectionString = str_replace('{{'.$key.'}}',$value,$connectionString);
+    }
+
+    $connectionString = str_replace('{{ROOT}}',dirname(__FILE__).DIRECTORY_SEPARATOR,$connectionString);
+	try{
+		$connection = new PDO($connectionString, $_GET['login'], $_GET['password'],$connector::pdo_attributes());
+    }catch(Exception $e){
+    	$response['error'] = 'Connexion impossible : '.$e->getMessage();
+    }
+	echo json_encode($response);
+	exit();
+}
+
 try {
+
+	/*
+	Page customizable depuis n'importe quel app.json de plugin via la mention : 
+
+	"install": {
+		"css" : "css/install.css",
+		"js" : "js/install.js",
+		"action" : "install.php"
+	},
+
+	css : chemin relatif au plugin d'un fichier css custom d'install
+	js : chemin relatif au plugin d'un fichier js custom d'install
+	action : chemin vers un fichier php contenant le code qui doit s'executer en fin d'installation.
+
+	*/
+
+
 	date_default_timezone_set('Europe/Paris');
 	mb_internal_encoding('UTF-8');
 	require_once(__DIR__.'/function.php');
@@ -25,6 +62,22 @@ try {
 		$custom['pluginPath'] =  __DIR__.DIRECTORY_SEPARATOR.'plugin'.DIRECTORY_SEPARATOR.basename(dirname($app)).DIRECTORY_SEPARATOR;
 	}
 
+
+	/*
+	Installation en mode cli, exemple : 
+	php install.php "{\"connector\":\"Mysql\",\"host\":\"mysql-host\",\"login\":\"root\",\"password\":\"root\",\"name\":\"hackpoint\",\"root\":\"http:\/\/127.0.0.1\/hackpoint\"}"
+	*/
+	if(php_sapi_name() == 'cli'){
+		echo 'Headless install...'.PHP_EOL;
+		$parameters = json_decode($argv[1],true);
+		echo 'Parameters: '.PHP_EOL;
+		print_r($parameters);
+		echo 'Installing... '.PHP_EOL;
+		install_core($parameters,$custom);
+		echo 'Installation done. '.PHP_EOL;
+		exit();
+	}
+
 	?>
 
 	<!DOCTYPE html>
@@ -47,7 +100,12 @@ try {
 		<?php if(!empty($custom['css'])): ?>
 			<link href="<?php echo $custom['plugin'].$custom['css']; ?>" rel="stylesheet">
 		<?php endif; ?>
-		
+		<style>
+			body{
+				background: #f5f5f5;
+			}
+		</style>
+
 	</head>
 
 	<body>
@@ -80,103 +138,10 @@ try {
 		//if(!extension_loaded('gd') || !function_exists('gd_info'))  throw new Exception('L\'extension php GD2  est requise, veuillez installer GD2 (sous linux : <code>sudo apt-get install php5-gd && service apache2 restart</code>)');
 		//if(!in_array('sqlite',PDO::getAvailableDrivers())) throw new Exception('Le driver SQLITE est requis, veuillez installer sqlite3 (sous linux : <code>sudo apt-get install php5-sqlite && service apache2 restart</code>)');
 
-		if(isset($_['install'])){
-
-			$constantStream = file_get_contents(__DIR__.'/constant-sample.php');
-
-			if(!isset($_['host'])) $_['host'] = '';
-			if(!isset($_['login'])) $_['login'] = '';
-			if(!isset($_['password'])) $_['password'] = '';
-			if(!isset($_['database'])) $_['database'] = '';
-
-			$cryptKey = base64_encode(time().$_['login'].mt_rand(0,1000));
-
-			$constantStream = str_replace(
-				array("{{BASE_SGBD}}","{{BASE_HOST}}","{{BASE_NAME}}","{{BASE_LOGIN}}","{{BASE_PASSWORD}}","{{ROOT_URL}}","{{CRYPT_KEY}}"),
-				array($_['entity'],$_['host'],$_['name'],$_['login'],$_['password'],$_['root'],$cryptKey),$constantStream
-			);
-
-			file_put_contents(__DIR__.'/constant.php',$constantStream);
-
-			require_once(__DIR__.'/constant.php');
-			require_once(__ROOT__.'class'.SLASH.'Entity.class.php');
-
+		if(isset($_['connector'])){
 			
+			install_core($_,$custom);
 
-
-			//install entities
-			Entity::install(__ROOT__.'class');
-
-			global $conf,$myUser;
-			$conf = new Configuration();
-			$conf->getAll();
-
-			//create firm 
-			$firm = new Firm();
-			$firm->label = 'Établissement';
-			$firm->description = 'Établissement par défaut';
-			$firm->save();
-
-			//create admin rank
-			$rank = new Rank();
-			$rank->label = 'Administrateur';
-			$rank->description = 'Dispose de tous les accès';
-			$rank->save();
-
-			//create default user
-			$admin = new User();
-			$admin->login = 'admin';
-			$admin->password = User::password_encrypt('admin');
-			$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;
-			$userfirmrank->firm = $firm->id;
-			$userfirmrank->save();
-
-			$sections = array();
-			Plugin::callHook('section',array(&$sections));
-			foreach($sections as $section=>$description){
-				$right = new Right();
-				$right->rank = $rank->id;
-				$right->section = $section;
-				$right->read = true;
-				$right->edit = true;
-				$right->delete = true;
-				$right->configure = true;
-				$right->save();
-			}
-
-			$enablePlugins = array('fr.sys1.factory','fr.sys1.dashboard','fr.sys1.notification','fr.sys1.navigation');
-
-
-			if(!empty($custom['action'])) require_once($custom['pluginPath'].$custom['action']);
-
-			//Activation des plugins par défaut
-			foreach ($enablePlugins as  $plugin) {
-				Plugin::state($plugin,true);
-			} 
-
-			$states = Plugin::states();
-
-			//Activation des plugins pour les établissements
-			foreach(Firm::loadAll() as $firm){
-
-				foreach ($enablePlugins as  $plugin) {
-					$firms = $states[$plugin];	
-					$key = array_search($firm->id, $firms);
-					$firms[] = $firm->id;
-					$states[$plugin] = array_values($firms);
-					Plugin::states($states);
-				}
-			}
 			
 
 			?>
@@ -196,30 +161,51 @@ try {
 
 
 			?>
-			<div class="row">
-				<form class="col-md-12 mt-3" action="install.php" method="POST">
+			<div class="row justify-content-md-center">
+				<form class="col-md-6 mt-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>
-					<select class="form-control" name="entity" onchange="window.location='install.php?sgbd='+$(this).val()">
-						<option value="">-</option>
-						<?php foreach($entities as $class=>$label): ?>
-							<option <?php echo (isset($_['sgbd']) && $_['sgbd']==$class ? 'selected="selected"': '') ?> value="<?php echo $class ?>"><?php echo $label; ?></option>
-						<?php endforeach; ?>
-					</select><br/>
+					<p class="text-muted">Merci de bien vouloir remplir les champs ci-dessous</p>
+					
+
+					<div class="input-group mb-3">
+						<div class="input-group-prepend">
+					    	<label class="input-group-text" for="connector">Type de base</label>
+					    </div>
+						<select class="form-control" id="connector" name="connector" onchange="window.location='install.php?sgbd='+$(this).val()">
+							<option value="">-</option>
+							<?php foreach($entities as $class=>$label): ?>
+								<option <?php echo (isset($_['sgbd']) && $_['sgbd']==$class ? 'selected="selected"': '') ?> value="<?php echo $class ?>"><?php echo $label; ?></option>
+							<?php endforeach; ?>
+						</select>
+					</div>
 
 					<?php if(isset($_['sgbd']) && $_['sgbd']!=''): 
 						require_once(__DIR__.'/connector/'.$_['sgbd'].'.class.php');
 						foreach($_['sgbd']::fields() as $field): ?>
-							<label for="<?php echo $field['id']; ?>"><?php echo $field['label']; ?></label><br/>
+
+							<div class="input-group mb-3">
+								<div class="input-group-prepend">
+							    	<label class="input-group-text"  for="<?php echo $field['id']; ?>"><?php echo $field['label']; ?></label>
+							    </div>
+
+							
 							<?php if(!isset($field['comment'])): ?><small><?php echo $field['comment']; ?></small><br/><?php endif; ?>
-							<input type="text" class="form-control" value="<?php echo $field['default']; ?>" name="<?php echo $field['id']; ?>" id="<?php echo $field['id']; ?>"/><br/>
+							<input type="text" class="form-control" value="<?php echo $field['default']; ?>" name="<?php echo $field['id']; ?>" id="<?php echo $field['id']; ?>"/></div>
 						<?php endforeach;  ?>
 
-						<label for="root">Adresse web</label><br/>
-						<input type="text" class="form-control" name="root" id="root" value="<?php echo $root; ?>"/><br/>
+						<div class="input-group mb-3">
+							<div class="input-group-prepend">
+						    	<label class="input-group-text" for="root">Adresse web</label>
+						    </div>
+							<input type="text" class="form-control " name="root" id="root" value="<?php echo $root; ?>"/><br/>
+						</div>
+
+
+						<div class="btn btn-primary right" onclick="$(this).parent().get(0).submit()"><i class="far fa-check-circle"></i> Installer</div>
+						<div class="btn btn-dark right mr-2 btn-test" onclick="check_connection()"><i class="far fa-check-circle"></i> Tester la connexion</div>
+						
+						</div>
 
-						<input type="submit" class="btn btn-primary" value="Installer" name="install"><br/><br/>
 					<?php endif; ?>
 				</form>
 			</div>
@@ -251,6 +237,139 @@ try {
 	<?php if(!empty($custom['js'])): ?>
 		<link href="<?php echo $custom['plugin'].$custom['js']; ?>" rel="stylesheet">
 	<?php endif; ?>
+	<script type="text/javascript">
+			function check_connection(){
+				var data = $('form').toJson();
+				data.action = 'check_connection';
+				$('.btn-test').addClass('btn-preloader');
+				$.getJSON({
+					url :'install.php',
+					data : data,
+					success:function(response){
+						setTimeout(function(){
+							$('.btn-test').removeClass('btn-preloader');
+						},500);
+						if(response.error){
+							$.message('error',response.error);
+						}else{
+							$.message('success','Connecté à la base avec succès');
+						}
+					}
+				});
+			}
+	</script>
 
 </body>
-</html>
+</html>
+
+<?php 
+
+function install_core($parameters,$custom){
+
+
+	$constantStream = file_get_contents(__DIR__.'/constant-sample.php');
+
+	if(!isset($parameters['host'])) $parameters['host'] = '';
+	if(!isset($parameters['login'])) $parameters['login'] = '';
+	if(!isset($parameters['password'])) $parameters['password'] = '';
+	if(!isset($parameters['database'])) $parameters['database'] = '';
+
+	$cryptKey = base64_encode(time().$parameters['login'].mt_rand(0,1000));
+
+	
+
+	$tags = array_merge($parameters,array(
+		'cryptKey' => $cryptKey
+	));
+	foreach ($tags as $key => $value) {
+		$constantStream = str_replace("{{".$key."}}",$value,$constantStream);
+	}
+
+	file_put_contents(__DIR__.DIRECTORY_SEPARATOR.'constant.php',$constantStream);
+
+	require_once(__DIR__.DIRECTORY_SEPARATOR.'constant.php');
+	require_once(__ROOT__.'class'.SLASH.'Entity.class.php');
+
+	
+
+
+	//install entities
+	Entity::install(__ROOT__.'class');
+
+	global $conf,$myUser;
+	$conf = new Configuration();
+	$conf->getAll();
+
+	//create firm 
+	$firm = new Firm();
+	$firm->label = 'Établissement';
+	$firm->description = 'Établissement par défaut';
+	$firm->save();
+
+	//create admin rank
+	$rank = new Rank();
+	$rank->label = 'Administrateur';
+	$rank->description = 'Dispose de tous les accès';
+	$rank->save();
+
+	//create default user
+	$admin = new User();
+	$admin->login = 'admin';
+	$admin->password = User::password_encrypt('admin');
+	$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;
+	$userfirmrank->firm = $firm->id;
+	$userfirmrank->save();
+
+	$sections = array();
+	Plugin::callHook('section',array(&$sections));
+	foreach($sections as $section=>$description){
+		$right = new Right();
+		$right->rank = $rank->id;
+		$right->section = $section;
+		$right->read = true;
+		$right->edit = true;
+		$right->delete = true;
+		$right->configure = true;
+		$right->save();
+	}
+
+
+	$enablePlugins = array('fr.sys1.factory','fr.sys1.dashboard','fr.sys1.notification','fr.sys1.navigation');
+
+
+	if(!empty($custom['action'])) require_once($custom['pluginPath'].$custom['action']);
+
+	//Activation des plugins par défaut
+	foreach ($enablePlugins as  $plugin) {
+		if(!Plugin::exist($plugin)) continue;
+		Plugin::state($plugin,true);
+	} 
+
+	$states = Plugin::states();
+
+	//Activation des plugins pour les établissements
+	foreach(Firm::loadAll() as $firm){
+
+		foreach ($enablePlugins as  $plugin) {
+			if(!Plugin::exist($plugin)) continue;
+			$firms = $states[$plugin];	
+			$key = array_search($firm->id, $firms);
+			$firms[] = $firm->id;
+			$states[$plugin] = array_values($firms);
+			Plugin::states($states);
+		}
+	}
+			
+}
+
+?>

+ 509 - 0
js/filter.component.js

@@ -0,0 +1,509 @@
+/*		
+@author valentin.carruesco	 
+
+
+ * data-hide-filters: Si renseigné, masque la recherche,
+ * data-default: permet de renseigner des filtres par défaut à la recherche (au format attendu -> voir fonction php filer_set()),
+ * data-label: Libellé affiché dans la partie gauche de la recherche simple,
+ * data-slug : Si spécifié, la recherche devient enregistrable pour une réutilisation ultérieure,
+ * data-only-advanced : Si l'attribut est présent, cache la recherche simple et ouvre par defaut la recherche avancée
+ * data-autosearch (default: true) : si définit a false, ne lancera pas la fonction data-function automatiquement en fin de chargement du composant
+ 
+ nb : La touche shift enfoncée lors de l'indentation d'un filtre forcera la création d'un nouveau groupe au lieu de chercher une fusion de groupe.
+ nb 2: ce composant fonctionne avec les templates de type de filtres situés dans la page footer, exemple de type de filtre
+
+<div class="filter-value-block" data-value-type="dictionnary" data-value-selector=".filter-value:last-child">
+	<select class="form-control filter-operator border-0 text-primary">
+		<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-slug="{{slug}}" data-depth="{{depth}}" data-filter-type-value="{{filterTypeValue}}" class="form-control filter-value" data-disable-label></select>
+</div>	
+
+data-value-type="" : permet de définir le data-type si la donnée est un composant
+data-value-selector : selecteur permetant de choisir le(s) iputs représeant la valeur réele du filtre (le selecteur est scopé sur la ligne du filtre)
+data-values="0" : sur l'operateur, cela permet de multiplier ou anihiler le champs de valeurs (0: supression, 1(default) un champs de valeur , n : n champs d valeur )
+
+ */
+var FilterBox = function(input,options){
+	var input = $(input);
+	var object = input.data('component-object');
+	this.input = input;
+
+	//Premiere création du composant
+	if(object == null){
+		object = this;
+		object.shift = false;
+
+		//permet le forcing de création d'un nouveau groupe lors de l'appuis sur maj
+		$(document).keydown(function(e) {
+	        if((e.which | e.keyCode) != 16) return;
+	        object.shift = true;
+	    }).keyup(function(e) {
+	        if((e.which | e.keyCode) != 16) return;
+	        object.shift = false;
+	    });
+
+		object.data = options;
+		object.slug = input.attr('data-slug');
+
+		if(object.slug != null) {
+			$('.btn-search-save',object.box).removeClass('hidden');
+			$('.dropdown-divider',object.box).removeClass('hidden');
+			$('.btn-search-reset',object.box).removeClass('hidden');
+		}
+		
+		input.addClass('hidden');
+		object.box = $('.advanced-search-box.hidden').clone().removeClass('hidden');
+		input.after(object.box);
+		object.columns = [];
+		object.tpl = $('.criterias .condition.hidden',object.box).get(0).outerHTML;
+		input.data('component-object',object);
+		object.columns.push({label : ' - Choix filtre - ',value:''});
+		$('option',input).each(function(i,element){
+			object.columns.push({
+				label : $(element).text(),
+				type : $(element).attr('data-filter-type'),
+				value : $(element).val()
+			});
+		});
+		if(object.columns.length<=1){
+			$('.options', object.box).addClass('hidden');
+			$('.simple-search .input-group-append').removeClass('hidden');
+		}
+
+		object.box.on('change','.filter-column',function(){
+			var condition = $(this).closest('.condition');
+			object.filter_refresh(condition,true);
+		}).on('change','.filter-operator',function(){
+			var condition = $(this).closest('.condition');
+			object.filter_refresh(condition);
+		}).on('click','.filter-option .btn-add',function(){
+			var button = $(this);
+			var condition = button.closest('.condition');
+			object.filter_add(condition);
+		}).on('keyup click','.filter-keyword',function(){
+			// RESET KEYWORD
+			var posx = (this.offsetWidth + this.offsetLeft - 20);
+			var props = $(this).val()!='' ? {'left':posx,'opacity':'1'} : {'left':posx+30,'opacity':'0'}
+			$('#search-clear').css(props)
+		}).on('mouseover','li.condition',function(event){
+			event.stopPropagation();
+			event.preventDefault();
+			$(this).addClass('hover');
+		}).on('mouseout','li.condition',function(event){
+			event.stopPropagation();
+			event.preventDefault();
+			$(this).removeClass('hover');
+			//Indentation d'un groupe
+		}).on('click',' li.condition .btn-indent',function(){
+			var line = $(this).closest('li');
+	
+			//si un groupe existe AVANT la condition, on déplace celle ci en FIN de ce groupe
+			if(!object.shift && line.prev('.condition').find('>ul.group').length!=0){
+				group = line.prev('.condition').find('>ul.group')
+				group.append(line.detach());
+				//si un groupe existe APRES la condition, on déplace celle ci en DEBUT de ce groupe
+			}else if(!object.shift && line.next('.condition').find('>ul.group').length!=0){
+				group = line.next('.condition').find('>ul.group');
+				group.prepend(line.detach());
+				//si aucun groupe a proximité, on créé un nouveau groupe
+			}else{
+				//tweak js car jquery ne capte pas bien les selected sur les clones
+				line.find(':selected').attr('selected','selected');
+				var newline = line.clone();
+				var newGroup = $('<ul class="group"></ul>');
+				newline.prepend(newGroup);
+				newline.find('.filter-column,.filter-operator,.filter-value').remove();
+				line.after(newline);
+				newGroup.append(line.detach());
+				group = newGroup;
+			}
+
+			$(group).sortable({
+				axis : 'y',
+				handle: ".btn-move",
+			});
+		}).on('click',' li.condition .btn-unindent',function(){
+			var line = $(this).closest('li.condition');
+			var parent = line.closest('ul.group');
+			if(parent.hasClass('criterias')) return;
+
+			//en fonction de la position de l'item dans son group on définit si on le déplace apres ou avant le groupe d'ou on le sort.
+			var middleGroup = Math.trunc($("li.condition",parent).length / 2);
+			var index = $("li.condition",parent).index(line) +1 ;
+
+			var parentLine = parent.parent();
+			var current = line.detach();
+
+			if(index <= middleGroup){
+				parentLine.before(current);
+			}else{
+				parentLine.after(current);
+			}
+			//Supression auto du groupe si plus aucun condition à l'interieur
+			if(parent.find('.condition').length==0) parent.parent().remove();
+		}).on('click',' li .btn-delete',function(){
+			var line = $(this).closest('li.condition');
+			object.filter_remove(line);
+		}).on('click','.btn-search',function(){
+			object.search();
+		}).on('keypress','.filter-keyword, .filter-value input',function(e){
+			if((e.wich | e.keyCode) != 13) return;
+			object.search();
+		}).on('click','.btn-search-save',function(){
+			object.filter_save();
+		}).on('click','.btn-search-reset',function(){
+			if(!confirm('Êtes-vous sûr de supprimer tous vos filtres définitivement ?')) return;
+			$.urlParam('filters','');
+			$('.condition:visible',object.box).each(function(i,element){
+				object.filter_remove($(element));
+			});
+			object.filter_save();
+			$('.condition',object.box).removeClass('error');
+		}).on('click','.btn-search-clean',function(){
+			$.urlParam('filters','');
+			$('.condition:visible',object.box).each(function(i,element){
+				object.filter_remove($(element));
+			});
+			$('.condition',object.box).removeClass('error');
+		});
+
+		//ajout de la premiere condition
+		object.filter_add();
+		$('.advanced-button-search',object.box).click(function(){
+			object.box.toggleClass('advanced');
+		});
+
+	 	//Preset des filtres depuis l'url si défini
+	 	var filters = $.urlParam('filters');
+	 	if(filters && filters!=''){
+	 		filters = JSON.parse(atob(filters));
+		} else if (input.attr('data-default') && input.attr('data-default').length){
+			//Preset des filtres depuis l'attribut data-default
+	 		filters = JSON.parse(input.attr('data-default'));
+
+	 		//Récupération du type de filtre pour la colonne ciblée
+	 		$.each(filters.a, function(i, filter){
+	 			if(filter.c==null || !filter.c.length || (filter.t!=null && filter.t.length)) return;
+
+	 			var option = $('option[value="'+filter.c+'"]', object.input);
+	 			if(!option.length){
+	 				delete filters.a[i];
+	 				return;
+	 			}
+	 			filters.a[i]['t'] = option.attr('data-filter-type');
+	 			filters.a[i]['o'] = filter.o.toLowerCase();
+	 		});
+		}
+
+		if(filters && filters!='') {
+			filters.keyword = filters.k;
+			filters.advanced = filters.a;
+			delete filters.a;
+			delete filters.k;
+
+			filters.advanced = object.filter_rename_keys(filters.advanced,{j:'join',o:'operator',v:'value',t:'type',c:'column',g:'group'});
+
+	 		if(filters.keyword && filters.keyword!='') $('.filter-keyword',object.box).val(filters.keyword);
+	 		if(filters.advanced.length>0){
+	 			$(object.box).addClass('advanced');
+	 			object.filters(filters.advanced);
+	 			object.box.find('.condition:visible:eq(0)').remove();
+	 		}
+	 		object.load_callback();
+	 	}else{
+	 	
+	 		if(object.slug!=null && object.slug!=''){
+	 			$.action({
+	 				action : 'filter_load',
+	 				slug : object.slug,
+	 			},function(response){
+
+	 				if(!response.filters.advanced || response.filters.advanced.length == 0){
+	 					object.load_callback();
+	 					return;
+	 				}
+	 				var filters = response.filters.advanced;
+	 				object.box.addClass('advanced');
+	 				object.filter_recursive_set(null,filters);
+
+	 				object.box.find('.condition:visible:eq(0)').remove();
+	 				object.search();
+	 			});
+	 		}else{
+	 			object.load_callback();
+	 		}
+	 	}
+
+	 	$('#search-clear',object.box).click(function(){
+	 		$(".filter-keyword",object.box).val('').focus();
+	 		$(this).attr('style', '');
+	 		object.load_callback();
+	 	});
+
+	}else{
+		//Rafraichissement et/ou récuperation du composant
+		//on reload les données de l'input aux cas ou elles aient changées
+		object.data = input.data();
+	}
+
+	if(object.data && object.data.onlyAdvanced!=null){
+		$('.simple-search,.advanced-button-search',object.box).addClass('hidden');
+		object.box.addClass('advanced');
+	}
+	if(object.data && object.data.label != null && object.data.label.length) $('.simple-search .data-search-label',object.box).html(object.data.label);
+	if(object.data && object.data.hideFilters != null) setTimeout(function(){object.box.addClass('hidden');}, 0);
+}
+
+FilterBox.prototype.load_callback = function(){
+	if(this.data && this.data.function && (!this.data.hasOwnProperty('autosearch') || this.data.autosearch==true))
+		window[this.data.function]();
+};
+
+//Enregistrement des filtres
+FilterBox.prototype.filter_save = function(){
+	var object = this;
+	var data = object.filters();
+	$.action({
+		action : 'filter_save',
+		slug : object.slug,
+		filters : data
+	},function(response){
+		if(response.message != null)
+			$.message('info',response.message);
+	});
+};
+
+//Rafraichissement des données et de la structure d'un filtre
+FilterBox.prototype.filter_refresh = function(condition,refreshOperator,data){
+	var object = this;
+	var column = $('.filter-column select',condition);
+	var optionColumn = $('option:selected',column);
+	
+	if(data && data.column)
+		column.val(data.column);
+
+	var type = optionColumn.attr('data-filter-type');
+	var dataAttributes = object.input.find('option[value="'+column.val()+'"]').data();
+	var operator = $('>.filter-operator',condition);
+	var operatorSelect = $('select',operator);
+	
+	if(column.val()==""){
+		 $('.filter-operator,.filter-value',condition).html('');
+		return;
+	}
+	
+	if(!type) return;
+	if(operator.length==0 || refreshOperator){
+		var operatorSelect = $($('.filter-value-block[data-value-type="'+type+'"] > .filter-operator').get(0).outerHTML);
+		//On normalise en lowercase les opérateurs
+		$('option', operatorSelect).val(function(){
+			return this.value.toLowerCase();
+		});
+		condition.find('> .filter-operator').html(operatorSelect);
+	}
+	if(data && data.operator) operatorSelect.val(data.operator);
+	if(data && data.join) $('>.filter-join',condition).val(data.join);
+
+	condition.find('.filter-value').html('');
+	var value = $('.filter-value-block[data-value-type="'+type+'"] .filter-value').get(0).outerHTML;
+	value = value.replace('data-template','data-type');
+	var repeat = $('option:selected',operator).attr('data-values');
+	repeat = repeat==null || repeat == '' ? 1 : repeat;
+
+	for(i=0;i<repeat;i++){
+		var valueInput = $(Mustache.render(value,dataAttributes));
+		condition.find('span.filter-value').append(valueInput);
+
+		//Si le type de champ est une liste on la remplit avec le datasource
+		if(dataAttributes.filterSource){
+			var source = dataAttributes.filterSource;
+			var options = '<option value=""> - </option>';
+			for (var k in source)
+				options += '<option value="'+k+'">'+source[k]+'</option>';
+			valueInput.append(options);
+		}
+		if(data && data.value!=null){
+			valueInput
+			.val(data.value[i])
+			.attr('data-value',data.value[i])
+			.trigger('change');
+		}
+	}
+	init_components(condition);
+}
+
+//lance la recherche (via le bouton)
+FilterBox.prototype.search = function(){
+	var object = this;
+	var filters = object.filters();
+
+	if(filters.advanced.length>0 || filters.keyword!=''){
+		
+		filters.k = filters.keyword;
+		filters.a = filters.advanced;
+
+		delete filters.advanced;
+		delete filters.keyword;
+
+		filters.a = object.filter_rename_keys(filters.a,{join:'j',operator:'o',value:'v',type:'t',column:'c',group:'g'});
+		filters = JSON.stringify(filters);
+	
+		if(!this.data.hasOwnProperty('urlsearch') || this.data.urlsearch==true)
+			$.urlParam('filters',btoa(filters));
+	}else{
+		$.urlParam('filters','');
+	}
+
+	window[object.data.function]();
+	if(object.data.callback) window[object.data.callback]();
+}
+
+//Renommage des clés des filtres (permet de compresser la chaine base64 encodée en url)
+FilterBox.prototype.filter_rename_keys = function(filters,mapping){
+	object = this;
+	var newFilters = [];
+	for(var k in filters){
+		newFilters[k] = {};
+		var keys = Object.keys(filters[k]);
+		for(var i in keys){
+			var key = keys[i];
+			if(mapping[key] === null) continue;
+			newFilters[k][mapping[key]] = filters[k][key];
+		}
+		if(newFilters[k].g) newFilters[k].g = object.filter_rename_keys(newFilters[k].g,mapping);
+		if(newFilters[k].group) newFilters[k].group = object.filter_rename_keys(newFilters[k].group,mapping);
+	}
+	return newFilters;
+}
+
+//définition ou récuperation d'un tableau de filtres
+FilterBox.prototype.filters = function(values,showErrors){
+	var object = this;
+	if(values){
+		object.filter_recursive_set($('.criterias',object.box),values);
+		return;
+	}
+
+	filters = object.filter_recursive_get($('.criterias',object.box));
+
+	return {
+		keyword : $('.filter-keyword',object.box).val(),
+		advanced : filters
+	};
+}
+
+//Définition des filtres dpeuis un objet values de manière récursive
+FilterBox.prototype.filter_recursive_set = function(parent,values){
+	var object = this;
+	var filters = [];
+	for(var key in values){
+		condition = object.filter_add(null,values[key],parent);
+		if(values[key].group){
+			var newGroup = $('<ul class="group"></ul>');
+			condition.find('.filter-column').after(newGroup);
+			condition.find('.filter-column').remove();
+			condition.find('.filter-operator').remove();
+			condition.find('.filter-value').remove();
+			if(values[key].join && values[key].join!='') condition.find('>.filter-join').val(values[key].join);
+			condition.prepend(newGroup);
+			object.filter_recursive_set(newGroup,values[key].group);
+		}
+	}		
+	return filters;
+}
+
+//Récuperation des diltres définis sur l'ui dans un object et d emanière récursive
+FilterBox.prototype.filter_recursive_get = function(parent){
+	var object = this;
+	var filters = [];
+	
+	$(parent).find('> .condition:visible').each(function(i,element){
+		var filter = {};
+		var element = $(element);
+
+		if(element.find('> .group').length>0){
+			filter.join = element.find('> .filter-join:visible').val();
+			filter.group = object.filter_recursive_get(element.find('> .group'));
+		}else{
+			filter.type = element.find('> .filter-column select option:selected').attr('data-filter-type');
+			var typeData = $('.filter-value-block[data-value-type="'+filter.type+'"]').data();
+			filter.column = element.find('> .filter-column select').val();
+
+			if(filter.column == null || filter.column == ''){
+				object.filter_remove(element);
+				return;
+			} else {
+				filter.operator = element.find('> .filter-operator select').val();
+				filter.value = [];
+				
+				if(typeData.valueSelector){
+					var valueElements = element.find('> .filter-value '+typeData.valueSelector);
+				}else{
+					var valueElements = element.find('> .filter-value > .filter-value');
+				}
+				
+				valueElements.each(function(u,input){
+					filter.value.push($(input).val());
+				});
+				filter.join = element.find('> .filter-join:visible').val();
+			}
+		}
+		if(!filter.group || filter.group.length) filters.push(filter);
+	});
+	return filters;
+}
+
+//Ajout d'un filtre visuel (vide ou replis avec l'obj data) dans un parent (optionnel) ou après un element (optionnel)
+//si pas de parent ou d'élements définis, le filtre s'ajoute au premier niveau.
+FilterBox.prototype.filter_add = function(element,data,parent){
+	var object = this;
+	if(!data) data = {};
+	data.columns = object.columns;
+
+	var condition = $(Mustache.render(object.tpl,data));
+	condition.removeClass('hidden');
+
+	if(element){
+		element.after(condition);
+	}else if(parent){
+		parent.append(condition);
+	}else{
+		$('.criterias',object.box).append(condition);
+	}
+	if(data) object.filter_refresh(condition,true,data);
+
+	$(condition.parent()).sortable({
+		axis : 'y',
+		handle: ".btn-move",
+	});
+	return condition;
+}
+
+//Supression d'un filtre et de ses parents vides.
+FilterBox.prototype.filter_remove = function(line){
+	var object = this;
+	var group = line.closest('ul.group');
+
+	//Si le filter est le dernier de la recherche, on reset ses champs sans le supprimer
+	if($('.criterias .condition:visible:has(>.filter-column)',object.box).length==1){
+		line.find('.filter-column select').prop('selectedIndex',0);
+		line.find('.filter-operator,.filter-value').html('');
+		line.find('.btn-unindent').trigger('click');
+		return;
+	}
+
+	//sinon on le supprime complétement
+	if($('.criterias > .condition:visible',object.box).length>1 || line.siblings('.condition:visible').length>0) line.remove();
+
+	//suppression ascendante récursive des groupes vides après suppression de la ligne
+	while(1){
+		if(!group || group.length == 0 || group.find('.condition').length!=0) break;
+		oldgroup = group.parent().parent();
+		group.parent().remove();
+		group = oldgroup;
+	}
+}

File diff suppressed because it is too large
+ 392 - 233
js/main.js


File diff suppressed because it is too large
+ 417 - 224
js/plugins.js


File diff suppressed because it is too large
+ 5 - 0
js/vendor/bootstrap.min.js


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


File diff suppressed because it is too large
+ 0 - 0
js/vendor/popper.min.js


+ 1039 - 0
js/vendor/trumbowyg.plugins.js

@@ -0,0 +1,1039 @@
+
+/* ===========================================================
+ * trumbowyg.pasteimage.js v1.0
+ * Basic base64 paste plugin for Trumbowyg
+ * http://alex-d.github.com/Trumbowyg
+ * ===========================================================
+ * Author : Alexandre Demode (Alex-D)
+ *          Twitter : @AlexandreDemode
+ *          Website : alex-d.fr
+ */
+
+
+ (function ($) {
+    'use strict';
+
+    $.extend(true, $.trumbowyg, {
+        plugins: {
+            pasteImage: {
+                init: function (trumbowyg) {
+                    trumbowyg.pasteHandlers.push(function (pasteEvent) {
+                        try {
+                            var items = (pasteEvent.originalEvent || pasteEvent).clipboardData.items,
+                            mustPreventDefault = false,
+                            reader;
+
+                            for (var i = items.length - 1; i >= 0; i -= 1) {
+                                if (items[i].type.match(/^image\//)) {
+                                    reader = new FileReader();
+                                    /* jshint -W083 */
+                                    reader.onloadend = function (event) {
+                                        trumbowyg.execCmd('insertImage', event.target.result, false, true);
+                                    };
+                                    /* jshint +W083 */
+                                    reader.readAsDataURL(items[i].getAsFile());
+
+                                    mustPreventDefault = true;
+                                }
+                            }
+
+                            if (mustPreventDefault) {
+                                pasteEvent.stopPropagation();
+                                pasteEvent.preventDefault();
+                            }
+                        } catch (c) {
+                        }
+                    });
+                }
+            }
+        }
+    });
+})(jQuery);
+
+
+/* ===========================================================
+ * trumbowyg.sys1.cleanpaste.js v1.0
+ * Clean les balises word
+ * ===========================================================
+ * Authors : Valentin CARRUESCO
+ */
+
+ (function ($) {
+    'use strict';
+
+    // clean editor
+    // this will clean the inserted contents
+    // it does a compare, before and after paste to determine the
+    // pasted contents
+    $.extend(true, $.trumbowyg, {
+        plugins: {
+            cleanPaste: {
+                init: function (trumbowyg) {
+                    trumbowyg.pasteHandlers.push(function () {
+                        setTimeout(function () {
+                          try {
+                                trumbowyg.$ed.find("*").filter(function(){
+                                    return /([^><]*):([^>]*)/i.test(this.nodeName);
+                                }).remove();
+                          } catch (c) {
+                          }
+                      }, 0);
+                    });
+                }
+            }
+        }
+    });
+})(jQuery);
+
+
+/* ===========================================================
+ * trumbowyg.fontfamily.js v1.2
+ */
+
+
+ (function ($) {
+    'use strict';
+
+    $.extend(true, $.trumbowyg, {
+        langs: {
+            // jshint camelcase:false
+            en: {
+                fontFamily: 'Font'
+            },
+            da: {
+                fontFamily: 'Skrifttype'
+            },
+            fr: {
+                fontFamily: 'Police'
+            },
+            de: {
+                fontFamily: 'Schriftart'
+            },
+            nl: {
+                fontFamily: 'Lettertype'
+            },
+            tr: {
+                fontFamily: 'Yazı Tipi'
+            },
+            zh_tw: {
+                fontFamily: '字體',
+            },
+            pt_br: {
+                fontFamily: 'Fonte',
+            },
+            ko: {
+                fontFamily: '글꼴'
+            },
+        }
+    });
+    // jshint camelcase:true
+
+    var defaultOptions = {
+        fontList: [
+        {name: 'Arial', family: 'Arial, Helvetica, sans-serif'},
+        {name: 'Arial Black', family: 'Arial Black, Gadget, sans-serif'},
+        {name: 'Comic Sans', family: 'Comic Sans MS, Textile, cursive, sans-serif'},
+        {name: 'Courier New', family: 'Courier New, Courier, monospace'},
+        {name: 'Georgia', family: 'Georgia, serif'},
+        {name: 'Impact', family: 'Impact, Charcoal, sans-serif'},
+        {name: 'Lucida Console', family: 'Lucida Console, Monaco, monospace'},
+        {name: 'Lucida Sans', family: 'Lucida Sans Uncide, Lucida Grande, sans-serif'},
+        {name: 'Palatino', family: 'Palatino Linotype, Book Antiqua, Palatino, serif'},
+        {name: 'Tahoma', family: 'Tahoma, Geneva, sans-serif'},
+        {name: 'Times New Roman', family: 'Times New Roman, Times, serif'},
+        {name: 'Trebuchet', family: 'Trebuchet MS, Helvetica, sans-serif'},
+        {name: 'Verdana', family: 'Verdana, Geneva, sans-serif'}
+        ]
+    };
+
+    // Add dropdown with web safe fonts
+    $.extend(true, $.trumbowyg, {
+        plugins: {
+            fontfamily: {
+                init: function (trumbowyg) {
+                    trumbowyg.o.plugins.fontfamily = $.extend({},
+                      defaultOptions,
+                      trumbowyg.o.plugins.fontfamily || {}
+                      );
+
+                    trumbowyg.addBtnDef('fontfamily', {
+                        dropdown: buildDropdown(trumbowyg),
+                        hasIcon: false,
+                        text: trumbowyg.lang.fontFamily
+                    });
+                }
+            }
+        }
+    });
+
+    function buildDropdown(trumbowyg) {
+        var dropdown = [];
+
+        $.each(trumbowyg.o.plugins.fontfamily.fontList, function (index, font) {
+            trumbowyg.addBtnDef('fontfamily_' + index, {
+                title: '<span style="font-family: ' + font.family + ';">' + font.name + '</span>',
+                hasIcon: false,
+                fn: function () {
+                    trumbowyg.execCmd('fontName', font.family, true);
+                }
+            });
+            dropdown.push('fontfamily_' + index);
+        });
+
+        return dropdown;
+    }
+})(jQuery);
+
+
+/* ===========================================================
+ * trumbowyg.mention.js v1.0
+ * Allow mention with @,# or any key
+ * http://sys1.fr
+ * ===========================================================
+ * Authors : Valentin CARRUESCO
+ */
+
+ (function ($) {
+    'use strict';
+
+    var o = {
+        //Supprime la totalité du mention si un backspace entame le mention
+        autoDelete : true,
+        //Ne déclenche la touche que si elle est précédée d'un espace, d'un saut de ligne ou d'une nouvelle balise
+        triggerBreak : true
+    };
+
+    $.extend(true, $.trumbowyg, {
+        plugins: {
+            mention: {
+                init: function (trumbowyg) {
+                    var object = this;
+                    object.altGrPressed  = false;
+                    trumbowyg.o.plugins.mention = $.extend(true, {},o,trumbowyg.o.plugins.mention || {});
+                    
+                    var keyMap = {
+                        48 : {
+                            label : '@',
+                            check : function(event){
+                               return object.altGrPressed;
+                            }
+                        },
+                        51 : {
+                            label : '#',
+                            check : function(event){
+                               return object.altGrPressed;
+                            }
+                        }
+                    }
+
+                    $(trumbowyg.$ta).parent().on('keydown', function (e) {
+                        var code = e.which || e.keyCode ;
+
+                        //verifie si alt+gr est enfoncé
+                        if(code==18){ 
+                            object.altGrPressed = true;
+                            return;
+                        }
+
+                       
+
+                        //Supprime les mentions en totalité sur un backspace
+                        if(trumbowyg.o.plugins.mention.autoDelete && code==8){
+                              var editor = trumbowyg.$ed;
+                              var textarea = trumbowyg.$ta;
+                              textarea.trumbowyg('saveRange');
+                              var parent = $(textarea.trumbowyg('getRange').endContainer.parentElement);
+                            if(parent.attr('data-mention-type')!= null){
+                                parent.remove();
+                                editor.trigger('keyup');
+                            }
+                        }
+
+                        //if(Object.keys(trumbowyg.o.plugins.mention.keys).indexOf(""+code) == -1) return;
+
+                        if(keyMap[code] == null) return;
+                        
+                        var char = keyMap[code].label;
+
+                        if(Object.keys(trumbowyg.o.plugins.mention.keys).indexOf(char) == -1) return;
+                        
+                        if(keyMap[code].check && !keyMap[code].check(event)) return;
+                        
+
+                       
+
+                        var editor = trumbowyg.$ed;
+                        var textarea = trumbowyg.$ta;
+                        var range = trumbowyg.$ta.trumbowyg('getRange');
+                        var lastChar = !range || !range.startContainer || !range.startContainer.data ?  '' : range.startContainer.data;
+                        lastChar = lastChar.length>0 ? lastChar.substring(lastChar.length-1) : '';
+                       
+                        //si le caractere trigger n'est pas lancé apres un espace ou un saut de ligne on ne fait rien
+                        if(trumbowyg.o.plugins.mention.triggerBreak && lastChar != '' && lastChar != ' ') return;
+                        
+                        var key = trumbowyg.o.plugins.mention.keys[char];
+                        if(!key.load) return;
+                        var data = {
+                            editor : editor,
+                            event : e,
+                            textarea : textarea,
+                            lastChar : lastChar
+                        }
+
+                        data.textarea.trumbowyg('saveRange');
+                        key.load(data);
+                    }).on('keyup', function (e) {
+                        var code = e.which || e.keyCode ;
+                        //désactive le altgr mode si relaché
+                        if(code==18){ 
+                            object.altGrPressed = false;
+                            return;
+                        }
+
+                    });
+                }
+                
+            }
+        }
+    })
+})(jQuery);
+
+
+/**
+Font size sys1, ne pas upgrade !!
+*/
+
+(function ($) {
+    'use strict';
+
+    $.extend(true, $.trumbowyg, {
+        langs: {
+            fr: {
+                fontsize: 'Taille de la police',
+                fontsizes: {
+                    'x-small': 'Très petit',
+                    'small': 'Petit',
+                    'medium': 'Normal',
+                    'large': 'Grand',
+                    'x-large': 'Très grand',
+                    'custom': 'Taille personnalisée'
+                },
+                fontCustomSize: {
+                    title: 'Taille de police personnalisée',
+                    label: 'Taille de la police',
+                    value: '48px'
+                }
+            }
+        }
+    });
+
+
+    // Add dropdown with font sizes
+    $.extend(true, $.trumbowyg, {
+        plugins: {
+            fontsize: {
+                init: function (trumbowyg) {
+                    trumbowyg.o.plugins.fontsize = $.extend({},
+                      {
+        sizeList: [
+            '8px',
+            '9px',
+            '10px',
+            '11px',
+            '12px',
+            '14px',
+            '16px',
+            '18px',
+            '20px',
+            '22px',
+            '24px',
+            '26px',
+            '28px',
+            '36px',
+            '48px',
+            '72px'
+        ],
+        allowCustomSize: true
+    },
+                      trumbowyg.o.plugins.fontsize || {}
+                      );
+
+                    trumbowyg.addBtnDef('fontsize', {
+                        dropdown: buildDropdown(trumbowyg)
+                    });
+                }
+            }
+        }
+    });
+
+    function setFontSize(trumbowyg, size) {
+        trumbowyg.$ed.focus();
+        trumbowyg.saveRange();
+        var text = trumbowyg.range.startContainer.parentElement;
+        var selectedText = trumbowyg.getRangeText();
+        if ($(text).html() === selectedText) {
+            $(text).css('font-size', size);
+        } else {
+            trumbowyg.range.deleteContents();
+            var html = '<span style="font-size:'+size+'">' + selectedText + '</span>';
+            var node = $(html)[0];
+            trumbowyg.range.insertNode(node);
+        }
+        trumbowyg.restoreRange();
+    }
+
+    function buildDropdown(trumbowyg) {
+        var dropdown = [];
+
+
+        if (trumbowyg.o.plugins.fontsize.allowCustomSize) {
+            var customSizeButtonName = 'fontsize_custom';
+            var customSizeBtnDef = {
+                fn: function () {
+                    trumbowyg.openModalInsert(trumbowyg.lang.fontCustomSize.title,
+                    {
+                        size: {
+                            label: trumbowyg.lang.fontCustomSize.label,
+                            value: trumbowyg.lang.fontCustomSize.value
+                        }
+                    },
+                    function (form) {
+                        setFontSize(trumbowyg, form.size);
+                        return true;
+                    }
+                    );
+                },
+                text: '<span style="font-size: medium;">' + trumbowyg.lang.fontsizes.custom + '</span>',
+                hasIcon: false
+            };
+            trumbowyg.addBtnDef(customSizeButtonName, customSizeBtnDef);
+            dropdown.push(customSizeButtonName);
+            $('.trumbowyg-dropdown-fontsize button').css({
+                height : '17px',
+                lineHeight : '17px',
+            });
+        }
+
+        $.each(trumbowyg.o.plugins.fontsize.sizeList, function (index, size) {
+            trumbowyg.addBtnDef('fontsize_' + size, {
+                text: '<span style="">' + (trumbowyg.lang.fontsizes[size] || size) + '</span>',
+                hasIcon: false,
+                fn: function () {
+                    setFontSize(trumbowyg, size);
+                }
+            });
+            dropdown.push('fontsize_' + size);
+        });
+
+        
+
+        return dropdown;
+    }
+})(jQuery);
+/* ===========================================================
+ * trumbowyg.colors.js v1.2
+ * Colors picker plugin for Trumbowyg
+ * http://alex-d.github.com/Trumbowyg
+ * ===========================================================
+ * Author : Alexandre Demode (Alex-D)
+ *          Twitter : @AlexandreDemode
+ *          Website : alex-d.fr
+ */
+
+ (function ($) {
+    'use strict';
+
+    $.extend(true, $.trumbowyg, {
+        langs: {
+            // jshint camelcase:false
+            cs: {
+                foreColor: 'Barva textu',
+                backColor: 'Barva pozadí'
+            },
+            en: {
+                foreColor: 'Text color',
+                backColor: 'Background color'
+            },
+            fr: {
+                foreColor: 'Couleur du texte',
+                backColor: 'Couleur de fond'
+            },
+            nl: {
+                foreColor: 'Tekstkleur',
+                backColor: 'Achtergrondkleur'
+            },
+            sk: {
+                foreColor: 'Farba textu',
+                backColor: 'Farba pozadia'
+            },
+            zh_cn: {
+                foreColor: '文字颜色',
+                backColor: '背景颜色'
+            },
+            ru: {
+                foreColor: 'Цвет текста',
+                backColor: 'Цвет выделения текста'
+            },
+            ja: {
+                foreColor: '文字色',
+                backColor: '背景色'
+            },
+            tr: {
+                foreColor: 'Yazı rengi',
+                backColor: 'Arkaplan rengi'
+            }
+        }
+    });
+
+    // jshint camelcase:true
+
+
+    function hex(x) {
+        return ('0' + parseInt(x).toString(16)).slice(-2);
+    }
+
+    function colorToHex(rgb) {
+        if (rgb.search('rgb') === -1) {
+            return rgb.replace('#', '');
+        } else if (rgb === 'rgba(0, 0, 0, 0)') {
+            return 'transparent';
+        } else {
+            rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))?\)$/);
+            return hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
+        }
+    }
+
+    function colorTagHandler(element, trumbowyg) {
+        var tags = [];
+
+        if (!element.style) {
+            return tags;
+        }
+
+        // background color
+        if (element.style.backgroundColor !== '') {
+            var backColor = colorToHex(element.style.backgroundColor);
+            if (trumbowyg.o.plugins.colors.colorList.indexOf(backColor) >= 0) {
+                tags.push('backColor' + backColor);
+            } else {
+                tags.push('backColorFree');
+            }
+        }
+
+        // text color
+        var foreColor;
+        if (element.style.color !== '') {
+            foreColor = colorToHex(element.style.color);
+        } else if (element.hasAttribute('color')) {
+            foreColor = colorToHex(element.getAttribute('color'));
+        }
+        if (foreColor) {
+            if (trumbowyg.o.plugins.colors.colorList.indexOf(foreColor) >= 0) {
+                tags.push('foreColor' + foreColor);
+            } else {
+                tags.push('foreColorFree');
+            }
+        }
+
+        return tags;
+    }
+
+    var defaultOptions = {
+        colorList: ['ffffff', '000000', 'eeece1', '1f497d', '4f81bd', 'c0504d', '9bbb59', '8064a2', '4bacc6', 'f79646', 'ffff00', 'f2f2f2', '7f7f7f', 'ddd9c3', 'c6d9f0', 'dbe5f1', 'f2dcdb', 'ebf1dd', 'e5e0ec', 'dbeef3', 'fdeada', 'fff2ca', 'd8d8d8', '595959', 'c4bd97', '8db3e2', 'b8cce4', 'e5b9b7', 'd7e3bc', 'ccc1d9', 'b7dde8', 'fbd5b5', 'ffe694', 'bfbfbf', '3f3f3f', '938953', '548dd4', '95b3d7', 'd99694', 'c3d69b', 'b2a2c7', 'b7dde8', 'fac08f', 'f2c314', 'a5a5a5', '262626', '494429', '17365d', '366092', '953734', '76923c', '5f497a', '92cddc', 'e36c09', 'c09100', '7f7f7f', '0c0c0c', '1d1b10', '0f243e', '244061', '632423', '4f6128', '3f3151', '31859b', '974806', '7f6000']
+    };
+
+    // Add all colors in two dropdowns
+    $.extend(true, $.trumbowyg, {
+        plugins: {
+            color: {
+                init: function (trumbowyg) {
+                    trumbowyg.o.plugins.colors = trumbowyg.o.plugins.colors || defaultOptions;
+                    var foreColorBtnDef = {
+                        dropdown: buildDropdown('foreColor', trumbowyg)
+                    },
+                    backColorBtnDef = {
+                        dropdown: buildDropdown('backColor', trumbowyg)
+                    };
+
+                    trumbowyg.addBtnDef('foreColor', foreColorBtnDef);
+                    trumbowyg.addBtnDef('backColor', backColorBtnDef);
+                },
+                tagHandler: colorTagHandler
+            }
+        }
+    });
+
+    function buildDropdown(fn, trumbowyg) {
+        var dropdown = [];
+
+        $.each(trumbowyg.o.plugins.colors.colorList, function (i, color) {
+            var btn = fn + color,
+            btnDef = {
+                fn: fn,
+                forceCss: true,
+                param: '#' + color,
+                style: 'background-color: #' + color + ';'
+            };
+            trumbowyg.addBtnDef(btn, btnDef);
+            dropdown.push(btn);
+        });
+
+        var removeColorButtonName = fn + 'Remove',
+        removeColorBtnDef = {
+            fn: 'removeFormat',
+            param: fn,
+            style: 'background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAG0lEQVQIW2NkQAAfEJMRmwBYhoGBYQtMBYoAADziAp0jtJTgAAAAAElFTkSuQmCC);'
+        };
+        trumbowyg.addBtnDef(removeColorButtonName, removeColorBtnDef);
+        dropdown.push(removeColorButtonName);
+
+        // add free color btn
+        var freeColorButtonName = fn + 'Free',
+        freeColorBtnDef = {
+            fn: function () {
+                trumbowyg.openModalInsert(trumbowyg.lang[fn],
+                {
+                    color: {
+                        label: fn,
+                        value: '#FFFFFF'
+                    }
+                },
+                        // callback
+                        function (values) {
+                            trumbowyg.execCmd(fn, values.color);
+                            return true;
+                        }
+                        );
+            },
+            text: '#',
+                // style adjust for displaying the text
+                style: 'text-indent: 0;line-height: 20px;padding: 0 5px;'
+            };
+            trumbowyg.addBtnDef(freeColorButtonName, freeColorBtnDef);
+            dropdown.push(freeColorButtonName);
+
+            return dropdown;
+        }
+    })(jQuery);
+
+
+
+/* ===========================================================
+ * trumbowyg.table.custom.js v2.0
+ * Table plugin for Trumbowyg
+ * http://alex-d.github.com/Trumbowyg
+ * ===========================================================
+ * Author : Sven Dunemann [dunemann@forelabs.eu]
+ */
+
+(function ($) {
+    'use strict';
+
+    var defaultOptions = {
+        rows: 8,
+        columns: 8,
+        styler: 'table'
+    };
+
+    $.extend(true, $.trumbowyg, {
+        langs: {
+            // jshint camelcase:false
+            en: {
+                table: 'Insert table',
+                tableAddRow: 'Add row',
+                tableAddRowAbove: 'Add row above',
+                tableAddColumnLeft: 'Add column to the left',
+                tableAddColumn: 'Add column to the right',
+                tableDeleteRow: 'Delete row',
+                tableDeleteColumn: 'Delete column',
+                tableDestroy: 'Delete table',
+                error: 'Error'
+            },
+            da: {
+                table: 'Indsæt tabel',
+                tableAddRow: 'Tilføj række',
+                tableAddRowAbove: 'Tilføj række',
+                tableAddColumnLeft: 'Tilføj kolonne',
+                tableAddColumn: 'Tilføj kolonne',
+                tableDeleteRow: 'Slet række',
+                tableDeleteColumn: 'Slet kolonne',
+                tableDestroy: 'Slet tabel',
+                error: 'Fejl'
+            },
+            de: {
+                table: 'Tabelle einfügen',
+                tableAddRow: 'Zeile hinzufügen',
+                tableAddRowAbove: 'Zeile hinzufügen',
+                tableAddColumnLeft: 'Spalte hinzufügen',
+                tableAddColumn: 'Spalte hinzufügen',
+                tableDeleteRow: 'Zeile löschen',
+                tableDeleteColumn: 'Spalte löschen',
+                tableDestroy: 'Tabelle löschen',
+                error: 'Error'
+            },
+            sk: {
+                table: 'Vytvoriť tabuľky',
+                tableAddRow: 'Pridať riadok',
+                tableAddRowAbove: 'Pridať riadok',
+                tableAddColumnLeft: 'Pridať stĺpec',
+                tableAddColumn: 'Pridať stĺpec',
+                error: 'Chyba'
+            },
+            fr: {
+                table: 'Insérer un tableau',
+                tableAddRow: 'Ajouter un ligne en dessous',
+                tableAddRowAbove: 'Ajouter une ligne au dessus',
+                tableAddColumnLeft: 'Ajouter une colonne à gauche',
+                tableAddColumn: 'Ajouter une colonne à droite',
+                tableDeleteRow: 'Effacer la ligne',
+                tableDeleteColumn: 'Effacer la colonne',
+                tableDestroy: 'Effacer le tableau',
+                error: 'Erreur'
+            },
+            cs: {
+                table: 'Vytvořit příkaz Table',
+                tableAddRow: 'Přidat řádek',
+                tableAddRowAbove: 'Přidat řádek',
+                tableAddColumnLeft: 'Přidat sloupec',
+                tableAddColumn: 'Přidat sloupec',
+                error: 'Chyba'
+            },
+            ru: {
+                table: 'Вставить таблицу',
+                tableAddRow: 'Добавить строку',
+                tableAddRowAbove: 'Добавить строку',
+                tableAddColumnLeft: 'Добавить столбец',
+                tableAddColumn: 'Добавить столбец',
+                tableDeleteRow: 'Удалить строку',
+                tableDeleteColumn: 'Удалить столбец',
+                tableDestroy: 'Удалить таблицу',
+                error: 'Ошибка'
+            },
+            ja: {
+                table: '表の挿入',
+                tableAddRow: '行の追加',
+                tableAddRowAbove: '行の追加',
+                tableAddColumnLeft: '列の追加',
+                tableAddColumn: '列の追加',
+                error: 'エラー'
+            },
+            tr: {
+                table: 'Tablo ekle',
+                tableAddRow: 'Satır ekle',
+                tableAddRowAbove: 'Satır ekle',
+                tableAddColumnLeft: 'Kolon ekle',
+                tableAddColumn: 'Kolon ekle',
+                error: 'Hata'
+            },
+            zh_tw: {
+                table: '插入表格',
+                tableAddRow: '加入行',
+                tableAddRowAbove: '加入行',
+                tableAddColumnLeft: '加入列',
+                tableAddColumn: '加入列',
+                tableDeleteRow: '刪除行',
+                tableDeleteColumn: '刪除列',
+                tableDestroy: '刪除表格',
+                error: '錯誤'
+            },
+            id: {
+                table: 'Sisipkan tabel',
+                tableAddRow: 'Sisipkan baris',
+                tableAddRowAbove: 'Sisipkan baris',
+                tableAddColumnLeft: 'Sisipkan kolom',
+                tableAddColumn: 'Sisipkan kolom',
+                tableDeleteRow: 'Hapus baris',
+                tableDeleteColumn: 'Hapus kolom',
+                tableDestroy: 'Hapus tabel',
+                error: 'Galat'
+            },
+            pt_br: {
+                table: 'Inserir tabela',
+                tableAddRow: 'Adicionar linha',
+                tableAddRowAbove: 'Adicionar linha',
+                tableAddColumnLeft: 'Adicionar coluna',
+                tableAddColumn: 'Adicionar coluna',
+                tableDeleteRow: 'Deletar linha',
+                tableDeleteColumn: 'Deletar coluna',
+                tableDestroy: 'Deletar tabela',
+                error: 'Erro'
+            },
+            ko: {
+                table: '표 넣기',
+                tableAddRow: '줄 추가',
+                tableAddRowAbove: '줄 추가',
+                tableAddColumnLeft: '칸 추가',
+                tableAddColumn: '칸 추가',
+                tableDeleteRow: '줄 삭제',
+                tableDeleteColumn: '칸 삭제',
+                tableDestroy: '표 지우기',
+                error: '에러'
+            },
+            // jshint camelcase:true
+        },
+
+        plugins: {
+            table: {
+                init: function (t) {
+                    t.o.plugins.table = $.extend(true, {}, defaultOptions, t.o.plugins.table || {});
+
+                    var buildButtonDef = {
+                        fn: function () {
+                            t.saveRange();
+
+                            var btnName = 'table';
+
+                            var dropdownPrefix = t.o.prefix + 'dropdown',
+                                dropdownOptions = { // the dropdown
+                                    class: dropdownPrefix + '-' + btnName + ' ' + dropdownPrefix + ' ' + t.o.prefix + 'fixed-top'
+                                };
+                            dropdownOptions['data-' + dropdownPrefix] = btnName;
+                            var $dropdown = $('<div/>', dropdownOptions);
+
+                            if (t.$box.find('.' + dropdownPrefix + '-' + btnName).length === 0) {
+                                t.$box.append($dropdown.hide());
+                            } else {
+                                $dropdown = t.$box.find('.' + dropdownPrefix + '-' + btnName);
+                            }
+
+                            // clear dropdown
+                            $dropdown.html('');
+
+                            // when active table show AddRow / AddColumn
+                            if (t.$box.find('.' + t.o.prefix + 'table-button').hasClass(t.o.prefix + 'active-button')) {
+                                $dropdown.append(t.buildSubBtn('tableAddRowAbove'));
+                                $dropdown.append(t.buildSubBtn('tableAddRow'));
+                                $dropdown.append(t.buildSubBtn('tableAddColumnLeft'));
+                                $dropdown.append(t.buildSubBtn('tableAddColumn'));
+                                $dropdown.append(t.buildSubBtn('tableDeleteRow'));
+                                $dropdown.append(t.buildSubBtn('tableDeleteColumn'));
+                                $dropdown.append(t.buildSubBtn('tableDestroy'));
+                            } else {
+                                var tableSelect = $('<table/>');
+                                $('<tbody/>').appendTo(tableSelect);
+                                for (var i = 0; i < t.o.plugins.table.rows; i += 1) {
+                                    var row = $('<tr/>').appendTo(tableSelect);
+                                    for (var j = 0; j < t.o.plugins.table.columns; j += 1) {
+                                        $('<td/>').appendTo(row);
+                                    }
+                                }
+                                tableSelect.find('td').on('mouseover', tableAnimate);
+                                tableSelect.find('td').on('mousedown', tableBuild);
+
+                                $dropdown.append(tableSelect);
+                                $dropdown.append($('<div class="trumbowyg-table-size">1x1</div>'));
+                            }
+
+                            t.dropdown(btnName);
+                        }
+                    };
+
+                    var tableAnimate = function(columnEvent) {
+                        var column = $(columnEvent.target),
+                            table = column.closest('table'),
+                            colIndex = this.cellIndex,
+                            rowIndex = this.parentNode.rowIndex;
+
+                        // reset all columns
+                        table.find('td').removeClass('active');
+
+                        for (var i = 0; i <= rowIndex; i += 1) {
+                            for (var j = 0; j <= colIndex; j += 1) {
+                                table.find('tr:nth-of-type('+(i+1)+')').find('td:nth-of-type('+(j+1)+')').addClass('active');
+                            }
+                        }
+
+                        // set label
+                        table.next('.trumbowyg-table-size').html((colIndex+1) + 'x' + (rowIndex+1));
+                    };
+
+                    var tableBuild = function() {
+                        t.saveRange();
+
+                        var tabler = $('<table/>');
+                        $('<tbody/>').appendTo(tabler);
+                        if (t.o.plugins.table.styler) {
+                            tabler.attr('class', t.o.plugins.table.styler);
+                        }
+
+                        var colIndex = this.cellIndex,
+                            rowIndex = this.parentNode.rowIndex;
+
+                        for (var i = 0; i <= rowIndex; i += 1) {
+                            var row = $('<tr></tr>').appendTo(tabler);
+                            for (var j = 0; j <= colIndex; j += 1) {
+                                $('<td/>').appendTo(row);
+                            }
+                        }
+
+                        t.range.deleteContents();
+                        t.range.insertNode(tabler[0]);
+                        t.$c.trigger('tbwchange');
+                    };
+
+                    var addRow = {
+                        title: t.lang.tableAddRow,
+                        text: t.lang.tableAddRow,
+                        ico: 'row-below',
+
+                        fn: function () {
+                            t.saveRange();
+
+                            var node = t.doc.getSelection().focusNode;
+                            var focusedRow = $(node).closest('tr');
+                            var table = $(node).closest('table');
+
+                            if(table.length > 0) {
+                                var row = $('<tr/>');
+                                // add columns according to current columns count
+                                for (var i = 0; i < table.find('tr')[0].childElementCount; i += 1) {
+                                    $('<td/>').appendTo(row);
+                                }
+                                // add row to table
+                                focusedRow.after(row);
+                            }
+
+                            t.syncCode();
+                        }
+                    };
+
+                    var addRowAbove = {
+                        title: t.lang.tableAddRowAbove,
+                        text: t.lang.tableAddRowAbove,
+                        ico: 'row-above',
+
+                        fn: function () {
+                            t.saveRange();
+
+                            var node = t.doc.getSelection().focusNode;
+                            var focusedRow = $(node).closest('tr');
+                            var table = $(node).closest('table');
+
+                            if(table.length > 0) {
+                                var row = $('<tr/>');
+                                // add columns according to current columns count
+                                for (var i = 0; i < table.find('tr')[0].childElementCount; i += 1) {
+                                    $('<td/>').appendTo(row);
+                                }
+                                // add row to table
+                                focusedRow.before(row);
+                            }
+
+                            t.syncCode();
+                        }
+                    };
+
+                    var addColumn = {
+                        title: t.lang.tableAddColumn,
+                        text: t.lang.tableAddColumn,
+                        ico: 'col-right',
+
+                        fn: function () {
+                            t.saveRange();
+
+                            var node = t.doc.getSelection().focusNode;
+                            var focusedCol = $(node).closest('td');
+                            var table = $(node).closest('table');
+                            var focusedColIdx = focusedCol.index();
+
+                            if(table.length > 0) {
+                                $(table).find('tr').each(function() {
+                                    $($(this).children()[focusedColIdx]).after('<td></td>');
+                                });
+                            }
+
+                            t.syncCode();
+                        }
+                    };
+
+                    var addColumnLeft = {
+                        title: t.lang.tableAddColumnLeft,
+                        text: t.lang.tableAddColumnLeft,
+                        ico: 'col-left',
+
+                        fn: function () {
+                            t.saveRange();
+
+                            var node = t.doc.getSelection().focusNode;
+                            var focusedCol = $(node).closest('td');
+                            var table = $(node).closest('table');
+                            var focusedColIdx = focusedCol.index();
+
+                            if(table.length > 0) {
+                                $(table).find('tr').each(function() {
+                                    $($(this).children()[focusedColIdx]).before('<td></td>');
+                                });
+                            }
+
+                            t.syncCode();
+                        }
+                    };
+
+                    var destroy = {
+                        title: t.lang.tableDestroy,
+                        text: t.lang.tableDestroy,
+                        ico: 'table-delete',
+
+                        fn: function () {
+                            t.saveRange();
+
+                            var node = t.doc.getSelection().focusNode,
+                                table = $(node).closest('table');
+
+                            table.remove();
+
+                            t.syncCode();
+                        }
+                    };
+
+                    var deleteRow = {
+                        title: t.lang.tableDeleteRow,
+                        text: t.lang.tableDeleteRow,
+                        ico: 'row-delete',
+
+                        fn: function () {
+                            t.saveRange();
+
+                            var node = t.doc.getSelection().focusNode,
+                                row = $(node).closest('tr');
+
+                            row.remove();
+
+                            t.syncCode();
+                        }
+                    };
+
+                    var deleteColumn = {
+                        title: t.lang.tableDeleteColumn,
+                        text: t.lang.tableDeleteColumn,
+                        ico: 'col-delete',
+
+                        fn: function () {
+                            t.saveRange();
+
+                            var node = t.doc.getSelection().focusNode,
+                                table = $(node).closest('table'),
+                                td = $(node).closest('td'),
+                                cellIndex = td.index();
+
+                            $(table).find('tr').each(function() {
+                                $(this).find('td:eq(' + cellIndex + ')').remove();
+                            });
+
+                            t.syncCode();
+                        }
+                    };
+
+                    t.addBtnDef('table', buildButtonDef);
+                    t.addBtnDef('tableAddRowAbove', addRowAbove);
+                    t.addBtnDef('tableAddRow', addRow);
+                    t.addBtnDef('tableAddColumnLeft', addColumnLeft);
+                    t.addBtnDef('tableAddColumn', addColumn);
+                    t.addBtnDef('tableDeleteRow', deleteRow);
+                    t.addBtnDef('tableDeleteColumn', deleteColumn);
+                    t.addBtnDef('tableDestroy', destroy);
+                }
+            }
+        }
+    });
+})(jQuery);

+ 1 - 1
lib/SimpleXLSX/SimpleXLSX.class.php

@@ -596,7 +596,7 @@ class SimpleXLSX {
 			$index  = 0;
 			for ( $i = $colLen - 1; $i >= 0; $i -- ) {
 				/** @noinspection PowerOperatorCanBeUsedInspection */
-				$index += ( ord( $col{$i} ) - 64 ) * pow( 26, $colLen - $i - 1 );
+				$index += ( ord( $col[$i] ) - 64 ) * pow( 26, $colLen - $i - 1 );
 			}
 			return array( $index - 1, $row - 1 );
 		}

+ 162 - 155
lib/XLSXWriter/XLSXWriter.class.php

@@ -1,5 +1,7 @@
-<?php 
-
+<?php
+/*
+ * @license MIT License
+ * */
 class XLSXWriter
 {
 	//http://www.ecma-international.org/publications/standards/Ecma-376.htm
@@ -12,6 +14,7 @@ class XLSXWriter
 	protected $title;
 	protected $subject;
 	protected $author;
+	protected $isRightToLeft;
 	protected $company;
 	protected $description;
 	protected $keywords = array();
@@ -23,14 +26,10 @@ class XLSXWriter
 	protected $number_formats = array();
 	public function __construct()
 	{
-		if(!ini_get('date.timezone'))
-		{
-			//using date functions can kick out warning if this isn't set
-			date_default_timezone_set('UTC');
-		}
-		$this->addCellStyle($number_format='GENERAL', $style_string=null);
-		$this->addCellStyle($number_format='GENERAL', $style_string=null);
-		$this->addCellStyle($number_format='GENERAL', $style_string=null);
+		defined('ENT_XML1') or define('ENT_XML1',16);//for php 5.3, avoid fatal error
+		date_default_timezone_get() or date_default_timezone_set('UTC');//php.ini missing tz, avoid warning
+		is_writeable($this->tempFilename()) or self::log("Warning: tempdir ".sys_get_temp_dir()." not writeable, use ->setTempDir()");
+		class_exists('ZipArchive') or self::log("Error: ZipArchive class does not exist");
 		$this->addCellStyle($number_format='GENERAL', $style_string=null);
 	}
 	public function setTitle($title='') { $this->title=$title; }
@@ -40,8 +39,9 @@ class XLSXWriter
 	public function setKeywords($keywords='') { $this->keywords=$keywords; }
 	public function setDescription($description='') { $this->description=$description; }
 	public function setTempDir($tempdir='') { $this->tempdir=$tempdir; }
+	public function setRightToLeft($isRightToLeft=false){ $this->isRightToLeft=$isRightToLeft; }
 	public function __destruct()
-	{ 
+	{
 		if (!empty($this->temp_files)) {
 			foreach($this->temp_files as $temp_file) {
 				@unlink($temp_file);
@@ -83,17 +83,17 @@ class XLSXWriter
 		}
 		$zip = new ZipArchive();
 		if (empty($this->sheets))                       { self::log("Error in ".__CLASS__."::".__FUNCTION__.", no worksheets defined."); return; }
-			if (!$zip->open($filename, ZipArchive::CREATE)) { self::log("Error in ".__CLASS__."::".__FUNCTION__.", unable to create zip."); return; }
-				$zip->addEmptyDir("docProps/");
-				$zip->addFromString("docProps/app.xml" , self::buildAppXML() );
-				$zip->addFromString("docProps/core.xml", self::buildCoreXML());
-				$zip->addEmptyDir("_rels/");
-				$zip->addFromString("_rels/.rels", self::buildRelationshipsXML());
-				$zip->addEmptyDir("xl/worksheets/");
-				foreach($this->sheets as $sheet) {
-					$zip->addFile($sheet->filename, "xl/worksheets/".$sheet->xmlname );
-				}
-				$zip->addFromString("xl/workbook.xml"         , self::buildWorkbookXML() );
+		if (!$zip->open($filename, ZipArchive::CREATE)) { self::log("Error in ".__CLASS__."::".__FUNCTION__.", unable to create zip."); return; }
+		$zip->addEmptyDir("docProps/");
+		$zip->addFromString("docProps/app.xml" , self::buildAppXML() );
+		$zip->addFromString("docProps/core.xml", self::buildCoreXML());
+		$zip->addEmptyDir("_rels/");
+		$zip->addFromString("_rels/.rels", self::buildRelationshipsXML());
+		$zip->addEmptyDir("xl/worksheets/");
+		foreach($this->sheets as $sheet) {
+			$zip->addFile($sheet->filename, "xl/worksheets/".$sheet->xmlname );
+		}
+		$zip->addFromString("xl/workbook.xml"         , self::buildWorkbookXML() );
 		$zip->addFile($this->writeStylesXML(), "xl/styles.xml" );  //$zip->addFromString("xl/styles.xml"           , self::buildStylesXML() );
 		$zip->addFromString("[Content_Types].xml"     , self::buildContentTypesXML() );
 		$zip->addEmptyDir("xl/_rels/");
@@ -122,6 +122,7 @@ class XLSXWriter
 			'freeze_columns' => $freeze_columns,
 			'finalized' => false,
 		);
+		$rightToLeftValue = $this->isRightToLeft ? 'true' : 'false';
 		$sheet = &$this->sheets[$sheet_name];
 		$tabselected = count($this->sheets) == 1 ? 'true' : 'false';//only first sheet is selected
 		$max_cell=XLSXWriter::xlsCell(self::EXCEL_2007_MAX_ROW, self::EXCEL_2007_MAX_COL);//XFE1048577
@@ -134,7 +135,7 @@ class XLSXWriter
 		$sheet->file_writer->write('<dimension ref="A1:' . $max_cell . '"/>');
 		$sheet->max_cell_tag_end = $sheet->file_writer->ftell();
 		$sheet->file_writer->write(  '<sheetViews>');
-		$sheet->file_writer->write(    '<sheetView colorId="64" defaultGridColor="true" rightToLeft="false" showFormulas="false" showGridLines="true" showOutlineSymbols="true" showRowColHeaders="true" showZeros="true" tabSelected="' . $tabselected . '" topLeftCell="A1" view="normal" windowProtection="false" workbookViewId="0" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100">');
+		$sheet->file_writer->write(    '<sheetView colorId="64" defaultGridColor="true" rightToLeft="'.$rightToLeftValue.'" showFormulas="false" showGridLines="true" showOutlineSymbols="true" showRowColHeaders="true" showZeros="true" tabSelected="' . $tabselected . '" topLeftCell="A1" view="normal" windowProtection="false" workbookViewId="0" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100">');
 		if ($sheet->freeze_rows && $sheet->freeze_columns) {
 			$sheet->file_writer->write(      '<pane ySplit="'.$sheet->freeze_rows.'" xSplit="'.$sheet->freeze_columns.'" topLeftCell="'.self::xlsCell($sheet->freeze_rows, $sheet->freeze_columns).'" activePane="bottomRight" state="frozen"/>');
 			$sheet->file_writer->write(      '<selection activeCell="'.self::xlsCell($sheet->freeze_rows, 0).'" activeCellId="0" pane="topRight" sqref="'.self::xlsCell($sheet->freeze_rows, 0).'"/>');
@@ -184,7 +185,7 @@ class XLSXWriter
 			$column_types[] = array('number_format' => $number_format,//contains excel format like 'YYYY-MM-DD HH:MM:SS'
 									'number_format_type' => $number_format_type, //contains friendly format like 'datetime'
 									'default_cell_style' => $cell_style_idx,
-								);
+									);
 		}
 		return $column_types;
 	}
@@ -223,7 +224,7 @@ class XLSXWriter
 	{
 		if (empty($sheet_name))
 			return;
-		self::initializeSheet($sheet_name);
+		$this->initializeSheet($sheet_name);
 		$sheet = &$this->sheets[$sheet_name];
 		if (count($sheet->columns) < count($row)) {
 			$default_column_types = $this->initializeColumnTypes( array_fill($from=0, $until=count($row), 'GENERAL') );//will map to n_auto
@@ -275,7 +276,7 @@ class XLSXWriter
 		}
 		$max_cell = self::xlsCell($sheet->row_count - 1, count($sheet->columns) - 1);
 		if ($sheet->auto_filter) {
-			$sheet->file_writer->write(    '<autoFilter ref="A1:' . $max_cell . '"/>');         
+			$sheet->file_writer->write(    '<autoFilter ref="A1:' . $max_cell . '"/>');			
 		}
 		$sheet->file_writer->write(    '<printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"/>');
 		$sheet->file_writer->write(    '<pageMargins left="0.5" right="0.5" top="1.0" bottom="1.0" header="0.5" footer="0.5"/>');
@@ -302,16 +303,18 @@ class XLSXWriter
 		$endCell = self::xlsCell($end_cell_row, $end_cell_column);
 		$sheet->merge_cells[] = $startCell . ":" . $endCell;
 	}
-	public function writeSheet(array $data, $sheet_name='', array $header_types=array(), $style=array())
+	public function writeSheet(array $data, $sheet_name='', array $header_types=array())
 	{
 		$sheet_name = empty($sheet_name) ? 'Sheet1' : $sheet_name;
 		$data = empty($data) ? array(array('')) : $data;
 		if (!empty($header_types))
-			isset($style['header']) ? $this->writeSheetHeader($sheet_name, $header_types, $style['header']) : $this->writeSheetHeader($sheet_name, $header_types);
-		
+		{
+			$this->writeSheetHeader($sheet_name, $header_types);
+		}
 		foreach($data as $i=>$row)
-			isset($style['content']) ? $this->writeSheetRow($sheet_name, $row, $style['content']) : $this->writeSheetRow($sheet_name, $row);
-
+		{
+			$this->writeSheetRow($sheet_name, $row);
+		}
 		$this->finalizeSheet($sheet_name);
 	}
 	protected function writeCell(XLSXWriter_BuffererWriter &$file, $row_number, $column_number, $value, $num_format_type, $cell_style_idx)
@@ -319,7 +322,7 @@ class XLSXWriter
 		$cell_name = self::xlsCell($row_number, $column_number);
 		if (!is_scalar($value) || $value==='') { //objects, array, empty
 			$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'"/>');
-		} elseif (is_string($value) && $value{0}=='='){
+		} elseif (is_string($value) && $value[0]=='='){
 			$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="s"><f>'.self::xmlspecialchars($value).'</f></c>');
 		} elseif ($num_format_type=='n_date') {
 			$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.intval(self::convert_date_time($value)).'</v></c>');
@@ -333,17 +336,17 @@ class XLSXWriter
 			if (!is_string($value) || $value=='0' || ($value[0]!='0' && ctype_digit($value)) || preg_match("/^\-?(0|[1-9][0-9]*)(\.[0-9]+)?$/", $value)){
 				$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.self::xmlspecialchars($value).'</v></c>');//int,float,currency
 			} else { //implied: ($cell_format=='string')
-			$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="inlineStr"><is><t>'.self::xmlspecialchars($value).'</t></is></c>');
+				$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="inlineStr"><is><t>'.self::xmlspecialchars($value).'</t></is></c>');
+			}
 		}
 	}
-}
-protected function styleFontIndexes()
-{
-	static $border_allowed = array('left','right','top','bottom');
-	static $border_style_allowed = array('thin','medium','thick','dashDot','dashDotDot','dashed','dotted','double','hair','mediumDashDot','mediumDashDotDot','mediumDashed','slantDashDot');
-	static $horizontal_allowed = array('general','left','right','justify','center');
-	static $vertical_allowed = array('bottom','center','distributed','top');
-	$default_font = array('size'=>'10','name'=>'Arial','family'=>'2');
+	protected function styleFontIndexes()
+	{
+		static $border_allowed = array('left','right','top','bottom');
+		static $border_style_allowed = array('thin','medium','thick','dashDot','dashDotDot','dashed','dotted','double','hair','mediumDashDot','mediumDashDotDot','mediumDashed','slantDashDot');
+		static $horizontal_allowed = array('general','left','right','justify','center');
+		static $vertical_allowed = array('bottom','center','distributed','top');
+		$default_font = array('size'=>'10','name'=>'Arial','family'=>'2');
 		$fills = array('','');//2 placeholders for static xml later
 		$fonts = array('','','','');//4 placeholders for static xml later
 		$borders = array('');//1 placeholder for static xml later
@@ -438,22 +441,22 @@ protected function styleFontIndexes()
 		foreach($this->number_formats as $i=>$v) {
 			$file->write('<numFmt numFmtId="'.(164+$i).'" formatCode="'.self::xmlspecialchars($v).'" />');
 		}
-		//$file->write(     '<numFmt formatCode="GENERAL" numFmtId="164"/>');
-		//$file->write(     '<numFmt formatCode="[$$-1009]#,##0.00;[RED]\-[$$-1009]#,##0.00" numFmtId="165"/>');
-		//$file->write(     '<numFmt formatCode="YYYY-MM-DD\ HH:MM:SS" numFmtId="166"/>');
-		//$file->write(     '<numFmt formatCode="YYYY-MM-DD" numFmtId="167"/>');
+		//$file->write(		'<numFmt formatCode="GENERAL" numFmtId="164"/>');
+		//$file->write(		'<numFmt formatCode="[$$-1009]#,##0.00;[RED]\-[$$-1009]#,##0.00" numFmtId="165"/>');
+		//$file->write(		'<numFmt formatCode="YYYY-MM-DD\ HH:MM:SS" numFmtId="166"/>');
+		//$file->write(		'<numFmt formatCode="YYYY-MM-DD" numFmtId="167"/>');
 		$file->write('</numFmts>');
 		$file->write('<fonts count="'.(count($fonts)).'">');
-		$file->write(       '<font><name val="Arial"/><charset val="1"/><family val="2"/><sz val="10"/></font>');
-		$file->write(       '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
-		$file->write(       '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
-		$file->write(       '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
+		$file->write(		'<font><name val="Arial"/><charset val="1"/><family val="2"/><sz val="10"/></font>');
+		$file->write(		'<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
+		$file->write(		'<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
+		$file->write(		'<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
 		foreach($fonts as $font) {
 			if (!empty($font)) { //fonts have 4 empty placeholders in array to offset the 4 static xml entries above
 				$f = json_decode($font,true);
 				$file->write('<font>');
-				$file->write(   '<name val="'.htmlspecialchars($f['name']).'"/><charset val="1"/><family val="'.intval($f['family']).'"/>');
-				$file->write(   '<sz val="'.intval($f['size']).'"/>');
+				$file->write(	'<name val="'.htmlspecialchars($f['name']).'"/><charset val="1"/><family val="'.intval($f['family']).'"/>');
+				$file->write(	'<sz val="'.intval($f['size']).'"/>');
 				if (!empty($f['color'])) { $file->write('<color rgb="'.strval($f['color']).'"/>'); }
 				if (!empty($f['bold'])) { $file->write('<b val="true"/>'); }
 				if (!empty($f['italic'])) { $file->write('<i val="true"/>'); }
@@ -464,8 +467,8 @@ protected function styleFontIndexes()
 		}
 		$file->write('</fonts>');
 		$file->write('<fills count="'.(count($fills)).'">');
-		$file->write(   '<fill><patternFill patternType="none"/></fill>');
-		$file->write(   '<fill><patternFill patternType="gray125"/></fill>');
+		$file->write(	'<fill><patternFill patternType="none"/></fill>');
+		$file->write(	'<fill><patternFill patternType="gray125"/></fill>');
 		foreach($fills as $fill) {
 			if (!empty($fill)) { //fills have 2 empty placeholders in array to offset the 2 static xml entries above
 				$file->write('<fill><patternFill patternType="solid"><fgColor rgb="'.strval($fill).'"/><bgColor indexed="64"/></patternFill></fill>');
@@ -473,7 +476,7 @@ protected function styleFontIndexes()
 		}
 		$file->write('</fills>');
 		$file->write('<borders count="'.(count($borders)).'">');
-		$file->write(    '<border diagonalDown="false" diagonalUp="false"><left/><right/><top/><bottom/><diagonal/></border>');
+        $file->write(    '<border diagonalDown="false" diagonalUp="false"><left/><right/><top/><bottom/><diagonal/></border>');
 		foreach($borders as $border) {
 			if (!empty($border)) { //fonts have an empty placeholder in the array to offset the static xml entry above
 				$pieces = json_decode($border,true);
@@ -482,7 +485,7 @@ protected function styleFontIndexes()
 				$file->write('<border diagonalDown="false" diagonalUp="false">');
 				foreach (array('left', 'right', 'top', 'bottom') as $side)
 				{
-					$show_side = in_array($side,$pieces['side']) ? true : false;
+                    $show_side = in_array($side,$pieces['side']) ? true : false;
 					$file->write($show_side ? "<$side style=\"$border_style\">$border_color</$side>" : "<$side/>");
 				}
 				$file->write(  '<diagonal/>');
@@ -491,35 +494,35 @@ protected function styleFontIndexes()
 		}
 		$file->write('</borders>');
 		$file->write('<cellStyleXfs count="20">');
-		$file->write(       '<xf applyAlignment="true" applyBorder="true" applyFont="true" applyProtection="true" borderId="0" fillId="0" fontId="0" numFmtId="164">');
-		$file->write(       '<alignment horizontal="general" indent="0" shrinkToFit="false" textRotation="0" vertical="bottom" wrapText="false"/>');
-		$file->write(       '<protection hidden="false" locked="true"/>');
-		$file->write(       '</xf>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="43"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="41"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="44"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="42"/>');
-		$file->write(       '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="9"/>');
+		$file->write(		'<xf applyAlignment="true" applyBorder="true" applyFont="true" applyProtection="true" borderId="0" fillId="0" fontId="0" numFmtId="164">');
+		$file->write(		'<alignment horizontal="general" indent="0" shrinkToFit="false" textRotation="0" vertical="bottom" wrapText="false"/>');
+		$file->write(		'<protection hidden="false" locked="true"/>');
+		$file->write(		'</xf>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="43"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="41"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="44"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="42"/>');
+		$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="9"/>');
 		$file->write('</cellStyleXfs>');
 		$file->write('<cellXfs count="'.(count($style_indexes)).'">');
-		//$file->write(     '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="164" xfId="0"/>');
-		//$file->write(     '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="165" xfId="0"/>');
-		//$file->write(     '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="166" xfId="0"/>');
-		//$file->write(     '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="167" xfId="0"/>');
+		//$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="164" xfId="0"/>');
+		//$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="165" xfId="0"/>');
+		//$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="166" xfId="0"/>');
+		//$file->write(		'<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="167" xfId="0"/>');
 		foreach($style_indexes as $v)
 		{
 			$applyAlignment = isset($v['alignment']) ? 'true' : 'false';
@@ -533,19 +536,19 @@ protected function styleFontIndexes()
 			$fontIdx = isset($v['font_idx']) ? intval($v['font_idx']) : 0;
 			//$file->write('<xf applyAlignment="'.$applyAlignment.'" applyBorder="'.$applyBorder.'" applyFont="'.$applyFont.'" applyProtection="false" borderId="'.($borderIdx).'" fillId="'.($fillIdx).'" fontId="'.($fontIdx).'" numFmtId="'.(164+$v['num_fmt_idx']).'" xfId="0"/>');
 			$file->write('<xf applyAlignment="'.$applyAlignment.'" applyBorder="'.$applyBorder.'" applyFont="'.$applyFont.'" applyProtection="false" borderId="'.($borderIdx).'" fillId="'.($fillIdx).'" fontId="'.($fontIdx).'" numFmtId="'.(164+$v['num_fmt_idx']).'" xfId="0">');
-			$file->write('  <alignment horizontal="'.$horizAlignment.'" vertical="'.$vertAlignment.'" textRotation="0" wrapText="'.$wrapText.'" indent="0" shrinkToFit="false"/>');
-			$file->write('  <protection locked="true" hidden="false"/>');
+			$file->write('	<alignment horizontal="'.$horizAlignment.'" vertical="'.$vertAlignment.'" textRotation="0" wrapText="'.$wrapText.'" indent="0" shrinkToFit="false"/>');
+			$file->write('	<protection locked="true" hidden="false"/>');
 			$file->write('</xf>');
 		}
 		$file->write('</cellXfs>');
-		$file->write(   '<cellStyles count="6">');
-		$file->write(       '<cellStyle builtinId="0" customBuiltin="false" name="Normal" xfId="0"/>');
-		$file->write(       '<cellStyle builtinId="3" customBuiltin="false" name="Comma" xfId="15"/>');
-		$file->write(       '<cellStyle builtinId="6" customBuiltin="false" name="Comma [0]" xfId="16"/>');
-		$file->write(       '<cellStyle builtinId="4" customBuiltin="false" name="Currency" xfId="17"/>');
-		$file->write(       '<cellStyle builtinId="7" customBuiltin="false" name="Currency [0]" xfId="18"/>');
-		$file->write(       '<cellStyle builtinId="5" customBuiltin="false" name="Percent" xfId="19"/>');
-		$file->write(   '</cellStyles>');
+		$file->write(	'<cellStyles count="6">');
+		$file->write(		'<cellStyle builtinId="0" customBuiltin="false" name="Normal" xfId="0"/>');
+		$file->write(		'<cellStyle builtinId="3" customBuiltin="false" name="Comma" xfId="15"/>');
+		$file->write(		'<cellStyle builtinId="6" customBuiltin="false" name="Comma [0]" xfId="16"/>');
+		$file->write(		'<cellStyle builtinId="4" customBuiltin="false" name="Currency" xfId="17"/>');
+		$file->write(		'<cellStyle builtinId="7" customBuiltin="false" name="Currency [0]" xfId="18"/>');
+		$file->write(		'<cellStyle builtinId="5" customBuiltin="false" name="Percent" xfId="19"/>');
+		$file->write(	'</cellStyles>');
 		$file->write('</styleSheet>');
 		$file->close();
 		return $temporary_filename;
@@ -571,8 +574,8 @@ protected function styleFontIndexes()
 		$core_xml.='<dc:creator>'.self::xmlspecialchars($this->author).'</dc:creator>';
 		if (!empty($this->keywords)) {
 			$core_xml.='<cp:keywords>'.self::xmlspecialchars(implode (", ", (array)$this->keywords)).'</cp:keywords>';
-		}       
-		$core_xml.='<dc:description>'.self::xmlspecialchars($this->description).'</dc:description>';        
+		}		
+		$core_xml.='<dc:description>'.self::xmlspecialchars($this->description).'</dc:description>';		
 		$core_xml.='<cp:revision>0</cp:revision>';
 		$core_xml.='</cp:coreProperties>';
 		return $core_xml;
@@ -609,7 +612,7 @@ protected function styleFontIndexes()
 			if ($sheet->auto_filter) {
 				$sheetname = self::sanitize_sheetname($sheet->sheetname);
 				$workbook_xml.='<definedName name="_xlnm._FilterDatabase" localSheetId="0" hidden="1">\''.self::xmlspecialchars($sheetname).'\'!$A$1:' . self::xlsCell($sheet->row_count - 1, count($sheet->columns) - 1, true) . '</definedName>';
-				$i++;   
+				$i++;	
 			}
 		}
 		$workbook_xml.='</definedNames>';
@@ -663,14 +666,15 @@ protected function styleFontIndexes()
 			$r = chr($n%26 + 0x41) . $r;
 		}
 		if ($absolute) {
-			return '$' . $r . '$' . ($row_number+1);            
+			return '$' . $r . '$' . ($row_number+1);
 		}
 		return $r . ($row_number+1);
 	}
 	//------------------------------------------------------------------
 	public static function log($string)
 	{
-		file_put_contents("php://stderr", date("Y-m-d H:i:s:").rtrim(is_array($string) ? json_encode($string) : $string)."\n");
+		//file_put_contents("php://stderr", date("Y-m-d H:i:s:").rtrim(is_array($string) ? json_encode($string) : $string)."\n");
+		error_log(date("Y-m-d H:i:s:").rtrim(is_array($string) ? json_encode($string) : $string)."\n");
 	}
 	//------------------------------------------------------------------
 	public static function sanitize_filename($filename) //http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx
@@ -686,7 +690,7 @@ protected function styleFontIndexes()
 		static $badchars  = '\\/?*:[]';
 		static $goodchars = '        ';
 		$sheetname = strtr($sheetname, $badchars, $goodchars);
-		$sheetname = substr($sheetname, 0, 31);
+		$sheetname = function_exists('mb_substr') ? mb_substr($sheetname, 0, 31) : substr($sheetname, 0, 31);
 		$sheetname = trim(trim(trim($sheetname),"'"));//trim before and after trimming single quotes
 		return !empty($sheetname) ? $sheetname : 'Sheet'.((rand()%900)+100);
 	}
@@ -712,57 +716,58 @@ protected function styleFontIndexes()
 		if ($num_format=='GENERAL') return 'n_auto';
 		if ($num_format=='@') return 'n_string';
 		if ($num_format=='0') return 'n_numeric';
-		if (preg_match("/[H]{1,2}:[M]{1,2}/i", $num_format)) return 'n_datetime';
-			if (preg_match("/[M]{1,2}:[S]{1,2}/i", $num_format)) return 'n_datetime';
-				if (preg_match("/[Y]{2,4}/i", $num_format)) return 'n_date';
-				if (preg_match("/[D]{1,2}/i", $num_format)) return 'n_date';
-				if (preg_match("/[M]{1,2}/i", $num_format)) return 'n_date';
-				if (preg_match("/$/", $num_format)) return 'n_numeric';
-				if (preg_match("/%/", $num_format)) return 'n_numeric';
-				if (preg_match("/0/", $num_format)) return 'n_numeric';
-				return 'n_auto';
-			}
+		if (preg_match('/[H]{1,2}:[M]{1,2}(?![^"]*+")/i', $num_format)) return 'n_datetime';
+		if (preg_match('/[M]{1,2}:[S]{1,2}(?![^"]*+")/i', $num_format)) return 'n_datetime';
+		if (preg_match('/[Y]{2,4}(?![^"]*+")/i', $num_format)) return 'n_date';
+		if (preg_match('/[D]{1,2}(?![^"]*+")/i', $num_format)) return 'n_date';
+		if (preg_match('/[M]{1,2}(?![^"]*+")/i', $num_format)) return 'n_date';
+		if (preg_match('/$(?![^"]*+")/', $num_format)) return 'n_numeric';
+		if (preg_match('/%(?![^"]*+")/', $num_format)) return 'n_numeric';
+		if (preg_match('/0(?![^"]*+")/', $num_format)) return 'n_numeric';
+		return 'n_auto';
+	}
 	//------------------------------------------------------------------
-			private static function numberFormatStandardized($num_format)
-			{
-				if ($num_format=='money') { $num_format='dollar'; }
-				if ($num_format=='number') { $num_format='integer'; }
-				if      ($num_format=='string')   $num_format='@';
-				else if ($num_format=='integer')  $num_format='0';
-				else if ($num_format=='date')     $num_format='YYYY-MM-DD';
-				else if ($num_format=='datetime') $num_format='YYYY-MM-DD HH:MM:SS';
-					else if ($num_format=='price')    $num_format='#,##0.00';
-					else if ($num_format=='dollar')   $num_format='[$$-1009]#,##0.00;[RED]-[$$-1009]#,##0.00';
-					else if ($num_format=='euro')     $num_format='#,##0.00 [$€-407];[RED]-#,##0.00 [$€-407]';
-					$ignore_until='';
-					$escaped = '';
-					for($i=0,$ix=strlen($num_format); $i<$ix; $i++)
-					{
-						$c = $num_format[$i];
-						if ($ignore_until=='' && $c=='[')
-							$ignore_until=']';
-						else if ($ignore_until=='' && $c=='"')
-							$ignore_until='"';
-						else if ($ignore_until==$c)
-							$ignore_until='';
-						if ($ignore_until=='' && ($c==' ' || $c=='-'  || $c=='('  || $c==')') && ($i==0 || $num_format[$i-1]!='_'))
-							$escaped.= "\\".$c;
-						else
-							$escaped.= $c;
-					}
-					return $escaped;
-				}
+	private static function numberFormatStandardized($num_format)
+	{
+		if ($num_format=='money') { $num_format='dollar'; }
+		if ($num_format=='number') { $num_format='integer'; }
+		if      ($num_format=='string')   $num_format='@';
+		else if ($num_format=='integer')  $num_format='0';
+		else if ($num_format=='date')     $num_format='YYYY-MM-DD';
+		else if ($num_format=='datetime') $num_format='YYYY-MM-DD HH:MM:SS';
+        else if ($num_format=='time')     $num_format='HH:MM:SS';
+		else if ($num_format=='price')    $num_format='#,##0.00';
+		else if ($num_format=='dollar')   $num_format='[$$-1009]#,##0.00;[RED]-[$$-1009]#,##0.00';
+		else if ($num_format=='euro')     $num_format='#,##0.00 [$€-407];[RED]-#,##0.00 [$€-407]';
+		$ignore_until='';
+		$escaped = '';
+		for($i=0,$ix=strlen($num_format); $i<$ix; $i++)
+		{
+			$c = $num_format[$i];
+			if ($ignore_until=='' && $c=='[')
+				$ignore_until=']';
+			else if ($ignore_until=='' && $c=='"')
+				$ignore_until='"';
+			else if ($ignore_until==$c)
+				$ignore_until='';
+			if ($ignore_until=='' && ($c==' ' || $c=='-'  || $c=='('  || $c==')') && ($i==0 || $num_format[$i-1]!='_'))
+				$escaped.= "\\".$c;
+			else
+				$escaped.= $c;
+		}
+		return $escaped;
+	}
 	//------------------------------------------------------------------
-				public static function add_to_list_get_index(&$haystack, $needle)
-				{
-					$existing_idx = array_search($needle, $haystack, $strict=true);
-					if ($existing_idx===false)
-					{
-						$existing_idx = count($haystack);
-						$haystack[] = $needle;
-					}
-					return $existing_idx;
-				}
+	public static function add_to_list_get_index(&$haystack, $needle)
+	{
+		$existing_idx = array_search($needle, $haystack, $strict=true);
+		if ($existing_idx===false)
+		{
+			$existing_idx = count($haystack);
+			$haystack[] = $needle;
+		}
+		return $existing_idx;
+	}
 	//------------------------------------------------------------------
 	public static function convert_date_time($date_input) //thanks to Excel::Writer::XLSX::Worksheet.pm (perl)
 	{
@@ -776,10 +781,10 @@ protected function styleFontIndexes()
 			list($junk,$year,$month,$day) = $matches;
 		}
 		if (preg_match("/(\d+):(\d{2}):(\d{2})/", $date_time, $matches))
-			{
-				list($junk,$hour,$min,$sec) = $matches;
-				$seconds = ( $hour * 60 * 60 + $min * 60 + $sec ) / ( 24 * 60 * 60 );
-			}
+		{
+			list($junk,$hour,$min,$sec) = $matches;
+			$seconds = ( $hour * 60 * 60 + $min * 60 + $sec ) / ( 24 * 60 * 60 );
+		}
 		//using 1900 as epoch, not 1904, ignoring 1904 special case
 		# Special cases for Excel.
 		if ("$year-$month-$day"=='1899-12-31')  return $seconds      ;    # Excel 1900 epoch
@@ -797,9 +802,12 @@ protected function styleFontIndexes()
 		$leap = (($year % 400 == 0) || (($year % 4 == 0) && ($year % 100)) ) ? 1 : 0;
 		$mdays = array( 31, ($leap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
 		# Some boundary checks
-		if($year < $epoch || $year > 9999) return 0;
-		if($month < 1     || $month > 12)  return 0;
-		if($day < 1       || $day > $mdays[ $month - 1 ]) return 0;
+		if ($year!=0 || $month !=0 || $day!=0)
+		{
+			if($year < $epoch || $year > 9999) return 0;
+			if($month < 1     || $month > 12)  return 0;
+			if($day < 1       || $day > $mdays[ $month - 1 ]) return 0;
+		}
 		# Accumulate the number of days since the epoch.
 		$days = $day;    # Add days for current month
 		$days += array_sum( array_slice($mdays, 0, $month-1 ) );    # Add days for past months
@@ -814,7 +822,6 @@ protected function styleFontIndexes()
 	}
 	//------------------------------------------------------------------
 }
-
 class XLSXWriter_BuffererWriter
 {
 	protected $fd=null;

+ 5 - 3
maintenance.php

@@ -1,5 +1,6 @@
 <?php
-require_once('common.php');
+require_once(__DIR__.DIRECTORY_SEPARATOR.'common.php');
+$mediaRoot = define_media_root();
 global $conf;
 if(file_exists('disabled.maintenance')){
 	header('Location: index.php');
@@ -16,7 +17,8 @@ if(file_exists('disabled.maintenance')){
 		<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
 		<link rel="shortcut icon" type="image/png" href="img/favicon.png" />
 		<link href="css/main.css" rel="stylesheet">
-		<link href="css/theme.css" rel="stylesheet" />
+		<!-- Plugin css files -->
+		<?php echo Plugin::callCss($mediaRoot,$cacheVersion); ?>
 	</head>
 	<body>
 	<?php if(isset($_['error']) && !empty($_['error'])): ?>
@@ -29,7 +31,7 @@ if(file_exists('disabled.maintenance')){
 		<div class="icon"><i class="fas fa-cog fa-spin"></i></div>
 		<h1>MAINTENANCE</h1>
 		<div class="container-fluid">
-			<div class="row justify-content-md-center">
+			<div class="row w-100 justify-content-md-center">
 				<div class="content-block col-md-6 col-sm-12">
 					<?php echo html_entity_decode(file_get_contents('enabled.maintenance')); ?>
 				</div>

+ 68 - 45
plugin/activedirectory/ActiveDirectory.class.php

@@ -7,8 +7,13 @@
  @description: Gestion des connexions aux annuaires ldap 
  */
 
-class ActiveDirectory
-{
+class ActiveDirectory{
+
+	const FORBIDDEN_CHARS = "/\\[]:;|=,+*?<>@";
+	const USER_SEARCH_DEFAULT_FILTER = "(&(objectClass=user)(objectCategory=person))";
+	//ldap_search retourne toujours le dn quelque soit le filtre utilisé
+	const USER_SEARCH_DEFAULT_ATTRIBUTES = "sn,givenname,mail,telephonenumber,mobile,title,samaccountname,department,thumbnailphoto,jpegphoto,accountexpires,memberof,manager,userprincipalname,whencreated";
+
 	public $server,$port,$login,$password,$userRoot,$groupRoot,$domain,$datasource,$protocolVersion;
 		
 
@@ -34,11 +39,11 @@ class ActiveDirectory
 	
 
 	/**
-	* Récupere un samaccountname depuis un CN (conrtion des liaisons ad en liasons erp)
+	* Récupere un samaccountname depuis un CN (conversion des liaisons ad en liasons erp)
 	* @param <Strong> CN complet de la personne recherchée
 	**/
     public function userFromCn($cn){
-		$entries= $this->search($this->userRoot,"(distinguishedname=$cn)");
+		$entries = $this->search($this->userRoot,'(distinguishedname='.$cn.')');
 		return $entries;
 	}
 
@@ -46,8 +51,8 @@ class ActiveDirectory
 	* Récupere un CN depuis le login
 	* @param <String> Login
 	**/
-	public function cnFromLogin($login){
-		$entries= $this->search($this->userRoot,'(samAccountName='.$login.')');
+	public function cnFromLogin($login,$attribute){
+		$entries= $this->search($this->userRoot,$this->authentification_filter($login,$attribute));
 		return (isset($entries[0])) ? $entries[0]['dn'] : false;
 	}
 
@@ -58,11 +63,11 @@ class ActiveDirectory
 	 * @return <void>
 	 */
 	function recursiveGroups(&$groups,$groupCN){
-		$entries = $this->search($this->groupRoot,"(member=".$groupCN.")");
+		$entries = $this->search($this->groupRoot,"(member=".$groupCN.")",'name');
 		if(count($entries)!=0 && $entries['count']!=0) {
 			if(isset($entries[0])){
 				$groups[] = $entries[0]['name'][0];
-				$parentCN = $entries[0]['distinguishedname'][0];
+				$parentCN = $entries[0]['dn'][0];
 				$this->recursiveGroups($groups,$parentCN);
 			}
 		}
@@ -74,39 +79,32 @@ class ActiveDirectory
 	 * Recherche des valeurs dans la base de données en fonction du filter
 	 * @param <String> Racine contexte de la recherche
 	 * @param <String> Filtre contenant les éléments a rechercher
+	 * @param <Array> Liste des attributs à récupérer
 	 * @return <Array> tableau contenant les objets correspondants a la recherche
 	 */
-	public function search($dn,$filter="(objectClass=user)"){
-		$roots = (substr_count($dn, ';') > 0) ? explode(';', $dn) : array($dn);
-		$infos = array('count' => 0);
-		foreach ($roots as $root) {
-			if(empty($root)) continue;
-			$sr = ldap_search($this->datasource, $root, $filter); // retourne un identifiant de recherche
-			if(!$sr) throw new Exception("Erreur lors de la recherche. Vérifiez que les racines existent");
-			$info = ldap_get_entries($this->datasource, $sr);
-
-			if($info['count'] == 0) continue;
-
-			$infos['count'] += $info['count'];
-			for($i=0;$i<$info['count'];$i++){
-				array_push($infos, $info[$i]);
-			}
-		}
-		$infos = array_unique($infos,SORT_REGULAR);
-		return $infos;
-	}
-	
-	/**
-	 * Récupere tous les utilisateurs du LDAP
-	 * @param <String> Racine contexte de la recherche
-	 */
-	public function populate($dn){
-		return $this->search($dn,"(&(samAccountName=*)(objectClass=user))");
-	}
+	public function search($dn, $filter=null, $attributes=null){
+		if(is_null($filter)) $filter = self::USER_SEARCH_DEFAULT_FILTER;
+		if(is_null($attributes)) $attributes = self::USER_SEARCH_DEFAULT_ATTRIBUTES;
+
+		$attributes = explode(',',$attributes);
+		$dns = (substr_count($dn, ';') > 0) ? explode(';', $dn) : array($dn);
+		$dataSources = array();
+
+		//Passage en tableau pour recherches parallèles
+		foreach($dns as $dn)
+			$dataSources[] = $this->datasource;		
 
-	public function get_domain_name($login){
-		$user = $this->search($this->userRoot,'(samAccountName='.$login.')');
-		return $user[0]['dn'];
+		$searches = ldap_search($dataSources, $dns, $filter, $attributes);
+
+		$infos = array();
+		foreach($searches as $search){
+			if($search === false) continue;
+	    	$info = ldap_get_entries($this->datasource, $search);
+	    	
+	    	for($i=0; $i<$info['count']; $i++)
+	       		$infos[] = $info[$i];
+	   	}
+	    return $infos;
 	}
 
 	public function set($dn,$entry,$value){
@@ -115,25 +113,52 @@ class ActiveDirectory
 			$attributes[$entry][0] = $value;
 			return ldap_modify($this->datasource,$dn,$attributes);
 		}else{
+			//evite le crash si l'attribut n'existe pas lors d'une supression
+			$attributes[$entry] ='0';
+			ldap_modify($this->datasource,$dn,$attributes);
 			$attributes[$entry] = array();
 			return ldap_mod_del($this->datasource,$dn,$attributes);
 		}
 	}
 
-	public function change_password( $userDn , $newPassword ) {
+	public function change_password($userDn, $newPassword){
 		if (!ldap_mod_replace($this->datasource, $userDn ,  self::encrypt_password($newPassword))) throw new Exception("Impossible de modifier le mot de passe : ".ldap_error($this->datasource));
 	}
 
-	public static function encrypt_password( $newPassword ) {
+	public static function encrypt_password($newPassword){
 		$newPassword = "\"" . $newPassword . "\"";
 		$len = strlen( $newPassword );
 		$newPassw = "";
 		for ( $i = 0; $i < $len; $i++ ){
-			$newPassw .= "{$newPassword{$i}}\000";
+			$newPassw .= "{".$newPassword[$i]."}\000";
 		}
 		return array("unicodePwd" => $newPassw);
 	}
 	
+	public function authentification_filter($login,$attribute){
+		$attributeName = self::authentification_attribute($attribute);
+		switch($attributeName){
+			case 'samaccountname':
+				$authentification = "(".$attributeName."=".$login.")";
+			break;
+			case 'userprincipalname':
+				$authentification = "(".$attributeName."=".$login.$this->domain.")";
+			break;
+			default:
+				$authentification = "(".$attributeName."=".$login.$this->domain.")";
+			break;
+		}
+
+		return '(&'.$authentification.'(objectClass=user)(objectCategory=person))';
+	}
+
+	public static function authentification_attribute($attribute){
+		return $attribute == '' ? $GLOBALS['setting']['activedirectory']['activedirectory_authentification']['default'] : $attribute;
+	}
+
+	public function attribute_to_login($attribute){
+		return str_replace(mb_strtolower($this->domain), '', $attribute);
+	}
 
 	/**
 	 * Deconnexion du LDAP
@@ -142,8 +167,6 @@ class ActiveDirectory
 		if($this->datasource!=null){
 			@ldap_close($this->datasource);
 		}
-	}
-	
-	
+	}	
 }
-?>
+?>

+ 2 - 2
plugin/activedirectory/action.php

@@ -45,8 +45,8 @@ switch($_['action']){
 					$response['tests']['reader-connection'] = true;
 
 				//Récupération users
-				$infos = $ldap->populate($conf->get('activedirectory_users_root'));
-				$response['tests']['users-connection'] = $infos["count"] == 0 ? false : true;
+				$infos = $ldap->search($conf->get('activedirectory_users_root'));
+				$response['tests']['users-connection'] = isset($infos) && !empty($infos);
 				$ldap->disconnect();
 			} catch (Exception $e) {
 				switch ($e->getCode()) {

+ 110 - 67
plugin/activedirectory/activedirectory.plugin.php

@@ -31,19 +31,26 @@ function ldap_plugin_all_users(&$users, $loadRights=false){
 	try{
 		$ldap = ldap_instance();
 		$ldap->connect($conf->get('activedirectory_reader_login'),$conf->get('activedirectory_reader_password'));
-		$infos = $ldap->populate($conf->get('activedirectory_users_root'));
 
-		if($infos["count"] == 0) return $ldap->disconnect();
+		$attributes = ActiveDirectory::USER_SEARCH_DEFAULT_ATTRIBUTES;
+		foreach(activedirectory_meta_data() as $data)
+			$attributes .= ','.$data['adslug'];
+
+		$infos = $ldap->search($conf->get('activedirectory_users_root'), null, $attributes);
+
+		if(empty($infos)) return $ldap->disconnect();
 		$allUsers = array();
+		$authentificationAttribute = ActiveDirectory::authentification_attribute($conf->get('activedirectory_authentification'));
+
 		foreach($infos as $info){
-			if( isset($info['userprincipalname'][0]) && trim($info['userprincipalname'][0])!=''){
+			if(isset($info[$authentificationAttribute][0]) && trim($info[$authentificationAttribute][0])!=''){
 				$newUser = new User();
 				ldap_user_fill($ldap,$newUser,$info,true,false);
 				if($loadRights) user_rank_firm_by_group($newUser);
 				$manager = new User();
 				if(isset($info['manager'][0])){
 					foreach($infos as $info2){
-						if(!isset($info2['distinguishedname'][0]) || $info2['distinguishedname'][0] != $info['manager'][0]) continue;		
+						if($info2['dn'] != $info['manager'][0]) continue;
 						ldap_user_fill($ldap,$manager,$info2,false,false);
 					}
 				}
@@ -67,27 +74,45 @@ function ldap_plugin_identification(&$user,$login,$password,$loadRight,$loadMana
 	if($user != false) return;
 	$ldap = ldap_instance();
 	try{
-		if($noPassword){
+		if($noPassword)
 			$ldap->connect($conf->get('activedirectory_reader_login'), $conf->get('activedirectory_reader_password'));
-		}else{
+		else
 			$ldap->connect($login.$ldap->domain, $password);
-		}
-		
-		$infos = $ldap->search($conf->get('activedirectory_users_root'),"(&(userprincipalname=".$login.$ldap->domain.")(objectClass=user))");
-		
-		if($infos["count"]>0){
+
+		$attributes = ActiveDirectory::USER_SEARCH_DEFAULT_ATTRIBUTES;
+		foreach(activedirectory_meta_data() as $data)
+			$attributes .= ','.$data['adslug'];
+
+		$infos = $ldap->search($conf->get('activedirectory_users_root'), $ldap->authentification_filter($login, $conf->get('activedirectory_authentification')), $attributes);
+
+		if(!empty($infos)){
 			$user = new User();
 			ldap_user_fill($ldap,$user,$infos[0],$loadRight,$loadManager);
 			user_rank_firm_by_group($user);
+			foreach($user->firms as $firm){
+				if($firm->has_plugin('fr.sys1.activedirectory')){
+					$firmHasPlugin = true;
+					break;
+				}
+			}
 		}
 
+		// if(!isset($firmHasPlugin)) throw new Exception("Ce compte n'est actif sur aucun établissement",403);
+
 		$avatarPath = __ROOT__.FILE_PATH.AVATAR_PATH.$user->login.'.jpg';
 		if(isset($user->meta['ldap_avatar'])){
 			if(!file_exists(__ROOT__.FILE_PATH.AVATAR_PATH)) mkdir(__ROOT__.FILE_PATH.AVATAR_PATH,0755,true);
 			file_put_contents($avatarPath,base64_decode($user->meta['ldap_avatar']));
 		}
 	}catch(Exception $e){
-		//nothing to do
+		switch ($e->getCode()) {
+			case 403:
+				// throw new Exception($e->getMessage(), 403);
+			break;
+
+			default:
+			break;
+		}
 	}
 	$ldap->disconnect();
 }
@@ -95,47 +120,45 @@ function ldap_plugin_identification(&$user,$login,$password,$loadRight,$loadMana
 
 //Remplissage d'une classe User en fonction des atttributs LDAPS
 function ldap_user_fill($ldap,&$user,$infos,$loadRight=false,$loadManager = false){
+	global $conf;
 	require_once(__DIR__.SLASH.'ActiveDirectory.class.php');
 	//Vérifie que le compte n'est pas expiré (nb : 0 et 9223372036854775807 sont les deux valeurs possibles pour un n'expire jamais (allez comprendre la logique microsoft...))
 	if(isset($infos['accountexpires'][0]) && $infos['accountexpires'][0]!=0 && $infos['accountexpires'][0]!=9223372036854775807){
 		//Convertion en seconds
-		$seconds = (float)($infos['accountexpires'][0] / 10000000); 
+		$seconds = (float)($infos['accountexpires'][0] / 10000000);
 		//Convertion d'un timestamp AD en timestamp UNIX
 		$timestamp = round($seconds - (((1970-1601) * 365.242190) * 86400));
 	    if($timestamp <= time()) return;
     }
-    
+
+    // récupération du login en fonction de l'attribut AD que l'on va contrôler
+    $attribute = ActiveDirectory::authentification_attribute($conf->get('activedirectory_authentification'));
+    if(isset($infos[$attribute][0])) $user->login = $ldap->attribute_to_login(mb_strtolower($infos[$attribute][0]));
+
 	if(isset($infos['sn'][0])) $user->setName($infos['sn'][0]);
 	if(isset($infos['givenname'][0])) $user->setFirstName($infos['givenname'][0]);
 	if(isset($infos['mail'][0])) $user->setMail($infos['mail'][0]);
 	if(isset($infos['telephonenumber'][0])) $user->setPhone($infos['telephonenumber'][0]);
 	if(isset($infos['mobile'][0])) $user->setMobile($infos['mobile'][0]);
 	if(isset($infos['title'][0])) $user->setFunction($infos['title'][0]);
-	if(isset($infos['samaccountname'][0])) $user->login = mb_strtolower($infos['samaccountname'][0]);
 	if(isset($infos['department'][0])) $user->service = $infos['department'][0];
 	if(isset($infos['thumbnailphoto'][0])) $user->meta['ldap_avatar'] = base64_encode($infos['thumbnailphoto'][0]);
 	if(isset($infos['jpegphoto'][0])) $user->meta['ldap_avatar'] = base64_encode($infos['jpegphoto'][0]);
-	
-	global $conf;
-	$metafields = explode(PHP_EOL,$conf->get('activedirectory_metafields'));
-	foreach ($metafields as $line) {
-		$metaInfos = explode(':',$line);
-		if(count($metaInfos)<4) continue;
-		list($label,$type,$adslug,$slug) = $metaInfos;
-		if(isset($infos[$adslug][0])) $user->meta[$slug] = $infos[$adslug][0];
-	}
+
+	foreach(activedirectory_meta_data() as $data)
+		if(isset($infos[$data['adslug']][0])) $user->meta[$data['slug']] = $infos[$data['adslug']][0];
 
 	if(isset($infos['whencreated'][0]) && strlen($infos['whencreated'][0])>=12 ){
 		$created = substr($infos['whencreated'][0],0,8).' '.substr($infos['whencreated'][0],8,2).':'.substr($infos['whencreated'][0],10,2);
-		$user->created = strtotime($created); 
+		$user->created = strtotime($created);
 	}
 
 	if(isset($infos['manager'][0])){
 		$user->manager = $infos['manager'][0];
 		if($loadManager){
 			$managerEntry = $ldap->userFromCn($infos['manager'][0]);
-			
-			if($managerEntry['count'] > 0 ){
+
+			if(isset($managerEntry) && !empty($managerEntry)){
 				$manager = new User();
 				ldap_user_fill($ldap,$manager,$managerEntry[0],$loadRight,false);
 				if(isset($managerEntry['sn'][0])) $manager->setName($managerEntry[0]['sn'][0]);
@@ -144,14 +167,13 @@ function ldap_user_fill($ldap,&$user,$infos,$loadRight=false,$loadManager = fals
 				if(isset($managerEntry['telephonenumber'][0])) $manager->setPhone($managerEntry[0]['telephonenumber'][0]);
 				if(isset($managerEntry['mobile'][0])) $manager->setMobile($managerEntry[0]['mobile'][0]);
 				if(isset($managerEntry['title'][0])) $manager->function = $managerEntry[0]['title'][0];
-				if(isset($managerEntry['samaccountname'][0])) $manager->login = mb_strtolower($managerEntry[0]['samaccountname'][0]);
+				if(isset($managerEntry[$attribute][0])) $manager->login = $ldap->attribute_to_login(mb_strtolower($managerEntry[0][$attribute][0]));
 				$user->manager = $manager;
 			}
 		}
 	}
 
 	$user->origin = 'active_directory';
-	
 
 	if($loadRight){
 		$groups = array();
@@ -162,12 +184,11 @@ function ldap_user_fill($ldap,&$user,$infos,$loadRight=false,$loadManager = fals
 				list($entity,$group) = explode('=',$group);
 				//TODO decommenter une fois les pb de perf résolus
 				//$ldap->recursiveGroups($groups,$groupCN);
-				$groups[] = $group; 
+				$groups[] = $group;
 			}
 		}
 		$user->groups = $groups;
 	}
-
 }
 
 
@@ -175,7 +196,7 @@ function ldap_user_fill($ldap,&$user,$infos,$loadRight=false,$loadManager = fals
 function activedirectory_user_save(&$user,$userForm,&$response){
 	if($user->origin != 'active_directory') return;
 	if($user->login != $userForm->login) throw new Exception("L'identifiant n'est pas modifiable");
-	
+
 	global $_,$conf;
 	require_once(__DIR__.SLASH.'ActiveDirectory.class.php');
 
@@ -190,28 +211,36 @@ function activedirectory_user_save(&$user,$userForm,&$response){
 
     $ldap = ldap_instance(true);
     if($conf->get('activedirectory_admin_login')=='') throw new Exception("Le compte AD admin n'est pas configuré, veuillez contacter un administrateur");
-	$ldap->connect($conf->get('activedirectory_admin_login'),$conf->get('activedirectory_admin_password'));	
-	$cn = $ldap->cnFromLogin($user->login);
+	$ldap->connect($conf->get('activedirectory_admin_login'),$conf->get('activedirectory_admin_password'));
+	$cn = $ldap->cnFromLogin($user->login, $conf->get('activedirectory_authentification'));
 	if(!$cn) throw new Exception("Impossible de trouver l'utilisateur dans la base active directory");
 
 	$user->phone = $userForm->phone;
 	$user->mobile = $userForm->mobile;
-	$infos = $ldap->search($conf->get('activedirectory_users_root'),"(&(userprincipalname=".$user->login.$ldap->domain.")(objectClass=user))");
 
-	if(in_array('telephonenumber', $infos[0]))
-		$ldap->set($cn,'telephonenumber',$userForm->phone);
+	$infos = $ldap->search($conf->get('activedirectory_users_root'),$ldap->authentification_filter($user->login, $conf->get('activedirectory_authentification')));
+
 	
-	if(in_array('mobile', $infos[0]))
-		$ldap->set($cn,'mobile',$userForm->mobile);
+	$ldap->set($cn,'telephonenumber',$userForm->phone);
+	$ldap->set($cn,'mobile',$userForm->mobile);
+
 	
-	if(in_array('jpegphoto', $infos[0])){
-		$avatarPath = __ROOT__.FILE_PATH.AVATAR_PATH.$user->login.'.jpg';
-		if(file_exists($avatarPath))
-			$ldap->set($cn,'jpegphoto',file_get_contents($avatarPath));
+	$avatarPath = glob(__ROOT__.FILE_PATH.AVATAR_PATH.$user->login.'.*');
+	if(!empty($avatarPath)){
+		$avatarPath = $avatarPath[0];
+		$temp = File::temp().rand(0,10000).basename($avatarPath);
+		copy($avatarPath,$temp);
+		Image::toJpg($temp);
+		$temp = explode('.', $temp);
+		array_pop($temp);
+		$temp = implode('.',$temp).'.jpg';
+		$ldap->set($cn,'jpegphoto',file_get_contents($temp));
+		unlink($temp);
 	}
 
 	if(!empty($userForm->password)){
 		$ldap->change_password($cn,$userForm->password);
+		$user->preference('passwordTime',time());
 	}
 
 	$response['warning'] = 'Vous êtes sur un compte de société, seules les informations suivantes ont été modifiées :<br/>
@@ -234,16 +263,18 @@ function user_rank_firm_by_group(&$user){
 		if(!in_array($group->adgroup,$user->groups)) continue;
 		$firm = $group->join('firm');
 		$rank = $group->join('rank');
+		if(is_null($firm->id)) continue;
 		$firms[$firm->id] = $firm;
 		if(!isset($ranks[$firm->id])) $ranks[$firm->id] = array();
 		$ranks[$firm->id][$rank->id] = $rank;
 	}
 
-	//Récuperation du rang par défaut
 	if(empty($ranks) && $conf->get('activedirectory_default_rank')!=''){
-		$firstFirm = Firm::load(array());
-		$firms[$firstFirm->id] = $firstFirm;
-		$ranks[$firstFirm->id][$conf->get('activedirectory_default_rank')] = Rank::getById($conf->get('activedirectory_default_rank'));
+		foreach(Firm::loadAll() as $firm){
+			if(!$firm->has_plugin('fr.sys1.activedirectory')) continue;
+			$firms[$firm->id] = $firm;
+			$ranks[$firm->id][$conf->get('activedirectory_default_rank')] = Rank::getById($conf->get('activedirectory_default_rank'));
+		}
 	}
 
 	if(!empty($ranks)) {
@@ -284,12 +315,12 @@ function activedirectory_plugin_section(&$sections){
 
 
 function activedirectory_plugin_install($id){
-	if($id != 'fr.idleman.activedirectory') return;
+	if($id != 'fr.sys1.activedirectory') return;
 	Entity::install(__DIR__);
 }
 
 function activedirectory_plugin_uninstall($id){
-	if($id != 'fr.idleman.activedirectory') return;
+	if($id != 'fr.sys1.activedirectory') return;
 	Entity::uninstall(__DIR__);
 }
 
@@ -303,24 +334,34 @@ function activedirectory_directory_list(&$usermapping){
 }
 
 function activedirectory_account_global(){
-	global $myUser,$conf;
-	$metafields = explode(PHP_EOL,$conf->get('activedirectory_metafields')); ?>
-	
+	global $myUser; ?>
+
 	<div class="row">
-	<?php foreach ($metafields as $line):
-		$metaInfos = explode(':',$line);
-		if(count($metaInfos)<4) continue;
-		list($label,$type,$adslug,$slug) = $metaInfos;
-	?>
+	<?php foreach(activedirectory_meta_data() as $data): ?>
 		<div class="col-md-6">
-			<label for="<?php echo $slug; ?>"><?php echo $label ?> :</label>
-			<input id="<?php echo $slug; ?>" name="<?php echo $slug; ?>" class="form-control-plaintext" readonly="readonly" type="text" value="<?php echo isset($myUser->meta[$slug])?$myUser->meta[$slug]:''; ?>">
+			<label for="<?php echo $data['slug']; ?>"><?php echo $data['label'] ?> :</label>
+			<input id="<?php echo $data['slug']; ?>" name="<?php echo $data['slug']; ?>" class="form-control-plaintext" readonly="readonly" type="text" value="<?php echo isset($myUser->meta[$data['slug']])?$myUser->meta[$data['slug']]:''; ?>">
 		</div>
 	<?php endforeach; ?>
+
 	</div>
 	<?php
 }
 
+function activedirectory_meta_data(){
+	global $conf;
+	$data = array();
+	$metaFields = preg_split('/('.PHP_EOL.'|\n|\r)/',$conf->get('activedirectory_metafields'),-1, PREG_SPLIT_NO_EMPTY);
+	$metas = array('label','type','adslug','slug');
+	foreach($metaFields as $line){
+		$metaInfos = explode(':',$line);
+		if(count($metaInfos)<4) continue;
+		$lineData = array_combine($metas,$metaInfos);
+		$data[] = $lineData;
+	}
+	return $data;
+}
+
 //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('activedirectory',array(
@@ -331,18 +372,20 @@ Configuration::setting('activedirectory',array(
     'activedirectory_domain' => array("label"=>"Domaine","type"=>"text","legend"=>"Le domaine sur lequel se base l'AD","placeholder"=>"@EXAMPLE.LOCAL"),
     'activedirectory_users_root' => array("label"=>'Racine des utilisateurs <small title="Cliquez pour ajouter une racine utilisateur supplémentaire" class="text-primary no-select right pointer" onclick="activedirectory_activedirectory_add_roots(this);"><i class="fas fa-plus"></i> Ajouter une racine supplémentaire</small>',"type"=>"text","legend"=>"La racine où chercher les users","placeholder"=>"OU=SYS1,OU=UTILISATEURS,OU=sys1.fr,DC=SYS1,DC=LOCAL","parameters"=>array("data-root"=>"users")),
     'activedirectory_groups_root' => array("label"=>'Racine des groupes <small title="Cliquez pour ajouter une racine groupe supplémentaire" class="text-primary no-select right pointer" onclick="activedirectory_activedirectory_add_roots(this);"><i class="fas fa-plus"></i> Ajouter une racine supplémentaire</small>',"type"=>"text","legend"=>"La racine où chercher les groupes","placeholder"=>"OU=SYS1,OU=UTILISATEURS,OU=sys1.fr,DC=SYS1,DC=LOCAL","parameters"=>array("data-root"=>"groups")),
-    
+
     "Compte Lecture seule",
     'activedirectory_reader_login' => array("label"=>"CN","type"=>"text","legend"=>"Le Common Name du compte de lecture seule","placeholder"=>"CN=reader_account,OU=EXAMPLE,OU=APPLICATIONS,OU=example.fr,..."),
     'activedirectory_reader_password' => array("label"=>"Mot de passe","type"=>"password","legend"=>"Le mot de passe du compte de lecture seule","placeholder"=>""),
-    
+
     "Compte Administrateur",
     'activedirectory_admin_login' => array("label"=>"CN","type"=>"text","legend"=>"Le Common Name du compte administrateur","placeholder"=>"CN=administrator_account,OU=EXAMPLE,OU=APPLICATIONS,OU=example.fr,..."),
     'activedirectory_admin_password' => array("label"=>"Mot de passe","type"=>"password","legend"=>"Le mot de passe du compte administrateur","placeholder"=>""),
-    
+
     "Champs de méta informations",
     'activedirectory_metafields' => array("label"=>"Méta informations","type"=>"textarea","legend"=>"Vous pouvez remplir des méta champs pour les utilisateurs (un champ par ligne).<br>
     Ces métas champs sont requis par certains plugins et peuvent être renseignés depuis l'AD via la syntaxe : <code>Libellé:Type:nom-champ-ad:nom-meta</code>","placeholder"=>"Date début contrat:date:description:jobstart","parameters"=>array("cols"=>"100")),
+    "Authentification",
+    'activedirectory_authentification' => array("label"=>"Authentification via l'attribut","type"=>"select","values"=>array("userprincipalname"=>"userPrincipalName","samaccountname"=>"sAMAccountName"),"default"=>"userprincipalname"),
     "Utilisateurs de l'AD",
     'activedirectory_default_rank' => array("label"=>"Rang par défaut","legend"=>"Utilisé si aucun groupe AD n'a été défini pour le rang \"Utilisateur\" standard","type"=>"rank")
 ));
@@ -353,15 +396,15 @@ Plugin::addJs('/js/main.js');
 Plugin::addCss('/css/main.css');
 
 Plugin::addHook('directory_list',"activedirectory_directory_list");
-Plugin::addHook("account_global", "activedirectory_account_global"); 
+Plugin::addHook("account_global", "activedirectory_account_global");
 Plugin::addHook("install", "activedirectory_plugin_install");
-Plugin::addHook("uninstall", "activedirectory_plugin_uninstall"); 
+Plugin::addHook("uninstall", "activedirectory_plugin_uninstall");
 Plugin::addHook("user_login", "ldap_plugin_identification");
 Plugin::addHook("user_load", "ldap_plugin_all_users");
-Plugin::addHook("user_save","activedirectory_user_save"); 
+Plugin::addHook("user_save","activedirectory_user_save");
 Plugin::addHook("user_rank_firm", "user_rank_firm_by_group");
 Plugin::addHook("section", "activedirectory_plugin_section");
 Plugin::addHook("action", "activedirectory_action");
 Plugin::addHook("menu_setting", "activedirectory_plugin_menu");
-Plugin::addHook("content_setting", "activedirectory_plugin_page");   
-?>
+Plugin::addHook("content_setting", "activedirectory_plugin_page");
+?>

+ 2 - 2
plugin/activedirectory/app.json

@@ -1,11 +1,11 @@
 {
-	"id": "fr.idleman.activedirectory",
+	"id": "fr.sys1.activedirectory",
 	"name": "Active Directory",
 	"author" : {
 		"name" : ""
 	},
 	"version": "1.0",
-	"url": "http://idleman.fr",
+	"url": "http://sys1.fr",
 	"licence": {"name": "Copyright","url" : ""},
 	"description": "Gestion du lien entre l'ERP et un AD. Permet l'utilisation de compte AD pour s'authentifier à l'ERP.",
 	"require" : {}

+ 147 - 128
plugin/activedirectory/setting.activedirectory.php

@@ -10,142 +10,161 @@ require_once(__DIR__.SLASH.'ActiveDirectoryGroup.class.php');
 		<h3>Réglages Active Directory</h3>
 		<div class="clear"></div>
 		<hr>
-		<?php echo Configuration::html('activedirectory'); ?>
-		<hr>
 	</div>
+</div>
 
-	<div class="activedirectory-test-connection col-md-12 mt-3 mb-3">
-		<div class="btn btn btn-primary float-right" onclick="activedirectory_connection_check();"><i class="fas fa-network-wired"></i> Tester la connexion</div>
-		<legend>Récapitulatif de connexion</legend>
-		<div class="clear"></div>
-		
-		<!-- Reload ajax partie bloc config -->
-		<div class="row">
-			<div class="col-sm-4">
-				<div id="reach-connection" class="card text-center">
-					<div class="card-body">
-						<div class="label">
-						    <h5 class="">Connexion au serveur AD distant</h5>
-						    <small class="card-subtitle mb-2 text-muted">Test de connexion au serveur</small>
-					    </div>
-					    <span class="d-block mt-2 mb-3">
-					    	<h2 class="card-icon d-block"><i class="fas fa-question-circle"></i></h2>
-					    </span>
-					    <small class="card-text text-muted">
-					    	<div>IP Serveur : <span class="text-<?php echo !empty($conf->get('activedirectory_server'))?'primary':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_server')) ? $conf->get('activedirectory_server') : "Non renseigné"; ?></span></div>
-					    	<div>Port / Port SSL : <span class="text-<?php echo !empty($conf->get('activedirectory_port')) && !empty($conf->get('activedirectory_ssl_port'))?'primary':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_port')) && !empty($conf->get('activedirectory_ssl_port')) ? $conf->get('activedirectory_port').' / '.$conf->get('activedirectory_ssl_port') : "Non renseigné"; ?></span></div>
-					    	<div>Domaine : <span class="text-<?php echo !empty($conf->get('activedirectory_domain'))?'primary':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_domain')) ? $conf->get('activedirectory_domain') : "Non renseigné"; ?></span></div>
-					    </small>
-					</div>
-				</div>
-			</div>
-			<div class="col-sm-4">
-				<div id="reader-connection" class="card text-center">
-					<div class="card-body">
-						<div class="label">
-						    <h5 class="">Connexion avec le compte AD <i>read-only</i></h5>
-						    <small class="card-subtitle mb-2 text-muted">Test d'identification avec le compte AD lecture seule</small>
-					    </div>
-					    <span class="d-block mt-2 mb-3">
-					    	<h2 class="card-icon d-block"><i class="fas fa-question-circle"></i></h2>
-					    </span>
-					    <small class="card-text text-muted">
-					    	<div class="text-fullbreak">CN : <span class="text-<?php echo !empty($conf->get('activedirectory_reader_login'))?'primary':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_reader_login')) ? $conf->get('activedirectory_reader_login') : "Non renseigné"; ?></span></div>
-					    	<div>Mot de passe : <span class="text-<?php echo !empty($conf->get('activedirectory_reader_password'))?'success':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_reader_password')) ? "Renseigné" : "Non renseigné"; ?></span></div>
-					    </small>
+<div class="row">
+	<div class="col-md-12">
+		<div class="tab-container noPrint">
+			<ul class="nav nav-tabs" role="tablist">
+				<li class="nav-item"><a data-toggle="tab" class="nav-link active" href="#tab-settings" aria-controls="tab-settings" aria-selected="false">Paramètres</a></li>
+				<li class="nav-item"><a data-toggle="tab" class="nav-link" href="#tab-mapping" aria-controls="tab-mapping" aria-selected="false">Correspondances</a></li>
+			</ul>
+		</div>
+
+		<div class="tab-content">
+			<!-- Onglet Général -->
+			<div class="tab-pane show active in" id="tab-settings" role="tabpanel" aria-labelledby="tab-settings"><br>
+				<?php echo Configuration::html('activedirectory'); ?>
+				<hr>
+				<div class="activedirectory-test-connection mt-3 mb-3">
+					<div class="btn btn btn-primary float-right" onclick="activedirectory_connection_check();"><i class="fas fa-network-wired"></i> Tester la connexion</div>
+					<legend>Récapitulatif de connexion</legend>
+					<div class="clear"></div>
+					
+					<!-- Reload ajax partie bloc config -->
+					<div class="row">
+						<div class="col-sm-4">
+							<div id="reach-connection" class="card text-center">
+								<div class="card-body">
+									<div class="label">
+									    <h5 class="">Connexion au serveur AD distant</h5>
+									    <small class="card-subtitle mb-2 text-muted">Test de connexion au serveur</small>
+								    </div>
+								    <span class="d-block mt-2 mb-3">
+								    	<h2 class="card-icon d-block"><i class="fas fa-question-circle"></i></h2>
+								    </span>
+								    <small class="card-text text-muted">
+								    	<div>IP Serveur : <span class="text-<?php echo !empty($conf->get('activedirectory_server'))?'primary':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_server')) ? $conf->get('activedirectory_server') : "Non renseigné"; ?></span></div>
+								    	<div>Port / Port SSL : <span class="text-<?php echo !empty($conf->get('activedirectory_port')) && !empty($conf->get('activedirectory_ssl_port'))?'primary':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_port')) && !empty($conf->get('activedirectory_ssl_port')) ? $conf->get('activedirectory_port').' / '.$conf->get('activedirectory_ssl_port') : "Non renseigné"; ?></span></div>
+								    	<div>Domaine : <span class="text-<?php echo !empty($conf->get('activedirectory_domain'))?'primary':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_domain')) ? $conf->get('activedirectory_domain') : "Non renseigné"; ?></span></div>
+								    </small>
+								</div>
+							</div>
+						</div>
+						<div class="col-sm-4">
+							<div id="reader-connection" class="card text-center">
+								<div class="card-body">
+									<div class="label">
+									    <h5 class="">Connexion avec le compte AD <i>read-only</i></h5>
+									    <small class="card-subtitle mb-2 text-muted">Test d'identification avec le compte AD lecture seule</small>
+								    </div>
+								    <span class="d-block mt-2 mb-3">
+								    	<h2 class="card-icon d-block"><i class="fas fa-question-circle"></i></h2>
+								    </span>
+								    <small class="card-text text-muted">
+								    	<div class="text-fullbreak">CN : <span class="text-<?php echo !empty($conf->get('activedirectory_reader_login'))?'primary':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_reader_login')) ? $conf->get('activedirectory_reader_login') : "Non renseigné"; ?></span></div>
+								    	<div>Mot de passe : <span class="text-<?php echo !empty($conf->get('activedirectory_reader_password'))?'success':'danger'; ?>"><?php echo !empty($conf->get('activedirectory_reader_password')) ? "Renseigné" : "Non renseigné"; ?></span></div>
+								    </small>
+								</div>
+							</div>
+						</div>
+						<div class="col-sm-4">
+							<div id="users-connection" class="card text-center">
+								<div class="card-body">
+									<div class="label">
+									    <h5 class="">Récupération d'utilisateurs</h5>
+									    <small class="card-subtitle mb-2 text-muted">Test de récupération d'utilisateurs depuis l'AD</small>
+								    </div>
+								    <span class="d-block mt-2 mb-3">
+								    	<h2 class="card-icon d-block"><i class="fas fa-question-circle"></i></h2>
+								    </span>
+								    <small class="card-text text-muted">
+								    	<div class="mb-2">Racine des utilisateurs : 
+								    		<?php if(!empty($conf->get('activedirectory_users_root'))): ?>
+											<ul class="pl-3 list-unstyled">
+												<?php foreach(explode(';',$conf->get('activedirectory_users_root')) as $userRoot): ?>
+												<li class="text-primary text-fullbreak"><?php echo $userRoot; ?></li>
+												<?php endforeach; ?>
+											</ul>
+											<?php else: ?>
+											<span class="text-danger">Non renseigné</span>
+											<?php endif; ?>
+								    	</div>
+								    	<div>Racine des groupes : 
+								    		<?php if(!empty($conf->get('activedirectory_groups_root'))): ?>
+											<ul class="pl-3 list-unstyled">
+												<?php foreach(explode(';',$conf->get('activedirectory_groups_root')) as $userRoot): ?>
+												<li class="text-primary text-fullbreak"><?php echo $userRoot; ?></li>
+												<?php endforeach; ?>
+											</ul>
+											<?php else: ?>
+											<span class="text-danger">Non renseigné</span>
+											<?php endif; ?>
+								    	</div>
+								    </small>
+								</div>
+							</div>
+						</div>
 					</div>
 				</div>
 			</div>
-			<div class="col-sm-4">
-				<div id="users-connection" class="card text-center">
-					<div class="card-body">
-						<div class="label">
-						    <h5 class="">Récupération d'utilisateurs</h5>
-						    <small class="card-subtitle mb-2 text-muted">Test de récupération d'utilisateurs depuis l'AD</small>
-					    </div>
-					    <span class="d-block mt-2 mb-3">
-					    	<h2 class="card-icon d-block"><i class="fas fa-question-circle"></i></h2>
-					    </span>
-					    <small class="card-text text-muted">
-					    	<div class="mb-2">Racine des utilisateurs : 
-					    		<?php if(!empty($conf->get('activedirectory_users_root'))): ?>
-								<ul class="pl-3 list-unstyled">
-									<?php foreach(explode(';',$conf->get('activedirectory_users_root')) as $userRoot): ?>
-									<li class="text-primary text-fullbreak"><?php echo $userRoot; ?></li>
-									<?php endforeach; ?>
-								</ul>
-								<?php else: ?>
-								<span class="text-danger">Non renseigné</span>
-								<?php endif; ?>
-					    	</div>
-					    	<div>Racine des groupes : 
-					    		<?php if(!empty($conf->get('activedirectory_groups_root'))): ?>
-								<ul class="pl-3 list-unstyled">
-									<?php foreach(explode(';',$conf->get('activedirectory_groups_root')) as $userRoot): ?>
-									<li class="text-primary text-fullbreak"><?php echo $userRoot; ?></li>
-									<?php endforeach; ?>
-								</ul>
-								<?php else: ?>
-								<span class="text-danger">Non renseigné</span>
-								<?php endif; ?>
-					    	</div>
-					    </small>
-					</div>
+
+			<div class="tab-pane" id="tab-mapping" role="mapping" aria-labelledby="tab-mapping"><br>
+				<div class="col-md-12">
+					<legend>Correspondance Groupe / Rang / Établissement</legend>
+					<table id="group-rank-firm" class="table table-striped table-bordered table-hover table-group-rank-firm">
+						<thead>
+							<tr>
+								<th>Groupe AD</th>
+								<th>Rang</th>
+								<th>Établissement</th>
+								<th></th>
+							</tr>
+							<tr id="ad-correspondance" data-id="" class="" data-action="activedirectory_group_save">
+								<th><input type="text" name="ad-group" id="ad-group" placeholder="Nom du groupe" class="form-control"></th>
+								<th>
+									<select name="ad-rank" id="ad-rank" class="form-control">
+										<option value="">-</option>
+										<?php foreach(Rank::loadAll() as $rank): ?>
+											<option value="<?php echo $rank->id; ?>"><?php echo $rank->label; ?></option>
+										<?php endforeach; ?>
+									</select>
+								</th>
+								<th>
+									<select name="ad-firm" id="ad-firm" class="form-control">
+										<option value="">-</option>
+										<?php foreach(Firm::loadAll() as $firm): ?>
+											<?php if($firm->has_plugin('fr.sys1.activedirectory')): ?>
+											<option value="<?php echo $firm->id; ?>"><?php echo $firm->label; ?></option>
+											<?php endif; ?>
+										<?php endforeach; ?>
+									</select>
+								</th>
+								<th class="text-center"><div class="btn btn-success" onclick="activedirectory_group_save();"><i class="fas fa-check"></i></div></th>
+							</tr>
+						</thead>
+						<tbody>
+							<tr data-id="{{id}}" class="hidden">
+								<td>{{adgroup}}</td>
+								<td>{{rankLabel}}</td>
+								<td>{{firmLabel}}</td>
+								<td class="text-center action-buttons">
+									<div class="btn btn-info btn-squarred btn-mini" onclick="activedirectory_group_edit(this);"><i class="fas fa-pencil-alt"></i></div>
+									<div class="btn btn-danger btn-squarred btn-mini" onclick="activedirectory_group_delete(this);"><i class="fas fa-times"></i></div>
+								</td>
+							</tr>
+						</tbody>
+					</table>
+					<!-- Pagination -->
+					<ul class="pagination justify-content-center">
+					    <li class="page-item hidden" data-value="{{value}}" title="Voir la page {{label}}" onclick="$(this).parent().find('li').removeClass('active');$(this).addClass('active');activedirectory_group_search()">
+					        <span class="page-link">{{label}}</span>
+					    </li>
+					</ul>
 				</div>
 			</div>
 		</div>
-		<hr>
-	</div>
-
-	<div class="col-md-12">
-		<legend>Correspondance Groupe / Rang / Établissement</legend>
-		<table id="group-rank-firm" class="table table-striped table-bordered table-hover table-group-rank-firm">
-			<thead>
-				<tr>
-					<th>Groupe AD</th>
-					<th>Rang</th>
-					<th>Établissement</th>
-					<th></th>
-				</tr>
-				<tr id="ad-correspondance" data-id="" class="" data-action="activedirectory_group_save">
-					<th><input type="text" name="ad-group" id="ad-group" placeholder="Nom du groupe" class="form-control"></th>
-					<th>
-						<select name="ad-rank" id="ad-rank" class="form-control">
-							<option value="">-</option>
-							<?php foreach(Rank::loadAll() as $rank): ?>
-								<option value="<?php echo $rank->id; ?>"><?php echo $rank->label; ?></option>
-							<?php endforeach; ?>
-						</select>
-					</th>
-					<th>
-						<select name="ad-firm" id="ad-firm" class="form-control">
-							<option value="">-</option>
-							<?php foreach(Firm::loadAll() as $firm): ?>
-								<option value="<?php echo $firm->id; ?>"><?php echo $firm->label; ?></option>
-							<?php endforeach; ?>
-						</select>
-					</th>
-					<th class="text-center"><div class="btn btn-success" onclick="activedirectory_group_save();"><i class="fas fa-check"></i></div></th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr data-id="{{id}}" class="hidden">
-					<td>{{adgroup}}</td>
-					<td>{{rankLabel}}</td>
-					<td>{{firmLabel}}</td>
-					<td class="text-center action-buttons">
-						<div class="btn btn-info btn-squarred btn-mini" onclick="activedirectory_group_edit(this);"><i class="fas fa-pencil-alt"></i></div>
-						<div class="btn btn-danger btn-squarred btn-mini" onclick="activedirectory_group_delete(this);"><i class="fas fa-times"></i></div>
-					</td>
-				</tr>
-			</tbody>
-		</table>
-		<!-- Pagination -->
-		<ul class="pagination justify-content-center">
-		    <li class="page-item hidden" data-value="{{value}}" title="Voir la page {{label}}" onclick="$(this).parent().find('li').removeClass('active');$(this).addClass('active');activedirectory_group_search()">
-		        <span class="page-link">{{label}}</span>
-		    </li>
-		</ul>
 	</div>
 </div>
 

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

@@ -411,6 +411,61 @@ div[data-type="dropzone"] > ul > li {
 
 }
 
+.btn-light {
+    color: #adbfd2;
+    background-color: #586573;
+    border-color: #586573;
+}
+.btn-light:not(:disabled):not(.disabled).active, .btn-light:not(:disabled):not(.disabled):active, .show>.btn-light.dropdown-toggle,.btn-light:hover {
+    color: #cbe0f7;
+    background-color: #748698;
+    border-color: #748698;
+}
+
+.advanced-search-box ul.group {
+    padding: 3px;
+    background-color: #3e4750;
+}
+.advanced-search-box.advanced .simple-search .filter-keyword {
+    background: #2b2f35;
+    color: #83b3ff;
+    border: 0;
+}
+.advanced-search-box.advanced .simple-search .data-search-label {
+    border-radius: 3px 0 0 0;
+    background: #2e363e;
+    border: 0;
+}
+.advanced-search-box li.condition.hover {
+    background-color: rgb(41, 47, 53);
+}
+
+.advanced-search-box.advanced .advanced-button-search i {
+    text-shadow: 0 0 5px #f0f7ff;
+    color: #f8fcff;
+}
+.advanced-search-box li.condition {
+    background: #343c44;
+    transition: background 0.2s ease-in-out;
+}
+.advanced-search-box li.condition {
+    background: #343c44;
+    transition: background 0.2s ease-in-out;
+}
+.advanced-search-box .filter-join {
+    border: 1px solid #2b2f35;
+    background: #2b2f35;
+    color: #9bf3af;
+}
+
+
+.advanced-search-box ul.group ul.group ,
+.advanced-search-box ul.group ul.group ul.group,
+.advanced-search-box ul.group ul.group ul.group ul.group,
+.advanced-search-box ul.group ul.group ul.group ul.group ul.group{
+    background-color: rgb(49, 53, 60);
+}
+
 @font-face {
     font-family: 'Muli';
     src: url('fonts/Muli-ExtraLight.woff2') format('woff2'),

+ 80 - 25
plugin/document/Element.class.php

@@ -31,12 +31,8 @@ class Element extends Entity{
 
 	//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(!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;
@@ -46,9 +42,8 @@ class Element extends Entity{
 			copy(self::root().$osPath,$thumbnail);
 			Image::resize($thumbnail,200,200);
 		}
-		
 		$path ='data:image/'.$this->extension.';base64,'.base64_encode(file_get_contents($thumbnail));
-	
+		
 		return $path;
 	}
 
@@ -140,7 +135,7 @@ class Element extends Entity{
 		
 		$element->label = mt_basename($path);
 		
-		$element->path = str_replace(self::root(),'',$path);
+		$element->path = str_replace(array(self::root(),'\\'),array('','/'),$path);
 		$element->type = is_dir($osPath) ? 'directory' : 'file';
 		$exists = file_exists($osPath);
 
@@ -162,7 +157,8 @@ class Element extends Entity{
 			$element->creator = $baseElement->creator;
 			$element->id = $baseElement->id;
 		}
-		$element->path = trim($element->path,SLASH);
+		$element->path = trim($element->path,'/');
+		
 		return $element;
 	}
 	
@@ -212,18 +208,21 @@ class Element extends Entity{
 
 				//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;
+				foreach(explode('/',$line->path) as $i=>$pathCrumb){
+					if($i!=0) $path.= '/';
 					$path.= $pathCrumb;
-					$pathes[] = str_replace("\\","\\\\",$path);
+					$pathes[] = $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
+				
+					
+					//Si les droits sont sur un dossier parent et sont recursifs 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;
 				}
@@ -371,7 +370,7 @@ class Element extends Entity{
 		$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);
+		if(!self::hasRight($parentTo,'edit')) throw new Exception("Permissions insuffisantes",403);
 	
 
 		$dbPath = str_replace('\\','/',$element->path);
@@ -425,7 +424,8 @@ class Element extends Entity{
 		$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);
+
+		if(!self::hasRight($parent,'edit')) throw new Exception("Permissions insuffisantes",403);
 
 		file_put_contents($osPath, $stream);
 
@@ -449,7 +449,7 @@ class Element extends Entity{
 		}
 		$parent = Element::fromPath($parentPath);
 
-		if($checkRight && !self::hasRight($parent,'read')) throw new Exception("Permissions insuffisantes",403);
+		if($checkRight && !self::hasRight($parent,'edit')) throw new Exception("Permissions insuffisantes",403);
 
 		if(file_exists($osPath)) return;
 		mkdir($osPath,0755,$recursive);
@@ -468,8 +468,6 @@ class Element extends Entity{
 				$documentRight = 'read';
 			break;
 			case 'delete':
-				$documentRight = 'edit';
-			break;
 			case 'edit':
 				$documentRight = 'edit';
 			break;
@@ -480,16 +478,44 @@ class Element extends Entity{
 
 		$allPathes = array();
 		$rootPath = '';
-		foreach (explode(SLASH,$element->path) as $i=>$crumb) {
-			$rootPath .= ($i==0?'':SLASH).$crumb;
-			$allPathes[] = str_replace("\\","\\\\",$rootPath);
+		foreach (explode('/',$element->path) as $i=>$crumb) {
+			$rootPath .= ($i==0?'':'/').$crumb;
+			$allPathes[] = $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=?) ';
+
+		$query = 'SELECT dr.recursive,dr.edit,dr.read,de.path FROM {{table}} dr LEFT JOIN '.Element::tableName().' de ON de.id = dr.element WHERE (dr.element IN(SELECT id from '.Element::tableName().' WHERE path IN("'.implode('","',$allPathes).'")) ) AND (dr.entity="all" OR (dr.entity="user" AND dr.uid=?) ';
+		
+		if(count($userRanks)!=0) $query .= ' OR (dr.entity="rank" AND dr.uid IN('.implode(',',$userRanks).')) ';
+		
+		$query .= ') ORDER BY CHAR_LENGTH(de.path)';
+		
+		$result = ElementRight::staticQuery($query,array($myUser->login));
+
+		$rights = array(
+			'edit'=> false,
+			'read'=> false
+		);
+		foreach($result->fetchAll() as $line){
+	
+			//si le droit n'est pas récursif et que le chemin associé n'est pas exactement cleui ciblé on ignore ce droit
+			if($line['recursive'] != 1  && $line['path']!=$element->path) continue;
+			
+			$rights = array(
+				'edit'=> $line['edit'] == 1,
+				'read'=> $line['read'] == 1
+			);
+			
+		}
+		
+		if(!$rights[$type]) return false;
+
+		//OLD
+		/*$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).')) ';
 		
@@ -497,12 +523,41 @@ class Element extends Entity{
 		
 		$result = ElementRight::staticQuery($query,array($myUser->login));
 		$result = $result->fetch();
-		if(!$result || $result['allowed']<=0) return false;
+		if(!$result || $result['allowed']<=0) return false;*/
+
+		//TEST
+		/*$checkRanks = count($userRanks) != 0 ? ' OR (entity="rank" AND uid IN('.implode(',',$userRanks).'))' : '';
+
+		$query = 'SELECT COUNT(id) allowed
+			FROM {{table}} 
+			WHERE element IN(
+				SELECT id AS parentId
+				FROM '.Element::tableName().' 
+				WHERE path = "'.$element->path.'"
+			)
+			AND `'.$documentRight.'` = 1
+			AND (entity="all" OR (entity="user" AND uid=:userLogin) '.$checkRanks.')
+			UNION
+			SELECT COUNT(id) allowed
+			FROM {{table}} 
+			WHERE element IN(
+				SELECT id AS parentId
+				FROM '.Element::tableName().' 
+				WHERE path IN("'.implode('","',$allPathes).'")
+			)
+			AND `'.$documentRight.'` = 1
+			AND (entity="all" OR (entity="user" AND uid=:userLogin) '.$checkRanks.')
+		';
+
+		$results = ElementRight::staticQuery($query,array('userLogin'=>$myUser->login));
+		foreach($results->fetchAll() as $id=>$result)
+			if(!$result['allowed'] || $result['allowed'] == 0) return false;*/
+
 		return true;
 	}
 
 	public static function remove_dir_recursive($path) {
-		foreach(glob($path.SLASH.'*') as $element){
+		foreach(glob($path.SLASH.'{,.}*',GLOB_BRACE) as $element){
 			if(mt_basename($element) == '.' || mt_basename($element) == '..') continue;
 			if(is_dir($element)) {
 				self::remove_dir_recursive($element);
@@ -516,4 +571,4 @@ class Element extends Entity{
 	
 
 }
-?>
+?>

+ 1 - 1
plugin/document/WebDav.class.php

@@ -527,7 +527,7 @@ class WebDav{
 			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.idleman:'.sha1($file);
+			$properties['token'] = isset($properties['token']) && $properties['token'] != '' ? $properties['token'] : 'fr.sys1:'.sha1($file);
 			$locks[base64_encode($file)] = $properties;
 		}
 

+ 29 - 74
plugin/document/action.php

@@ -1,12 +1,9 @@
 <?php
 global $_,$conf;
 switch($_['action']){
-
 	case 'document_load_template':
-		
 		global $myUser,$_;
 		require_once(__DIR__.SLASH.'template.document.php');
-		
 	break;
 
 	case 'document_widget_load':
@@ -15,7 +12,7 @@ switch($_['action']){
 		$widget = DashboardWidget::current();
 		
 		$root = $widget->data('widget-document-root');
-		$root = !empty($root) ? ': <strong>'.$root.')</strong>':'';
+		$root = !empty($root) ? ': <strong>'.$root.'</strong>':'';
 
 		$widget->title = 'Mes documents'.$root;
 		ob_start();
@@ -55,9 +52,6 @@ switch($_['action']){
 		echo $content ;
 	break;
 
-
-	
-
 	case 'document_embedded':
 		Action::write(function(&$response){
 			Plugin::addCss("/css/main.css"); 
@@ -73,7 +67,6 @@ switch($_['action']){
 		});
 	break;
 
-
 	case 'document_folder_create':
 		Action::write(function(&$response){
 			global $myUser,$_,$conf;
@@ -81,20 +74,16 @@ switch($_['action']){
 			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 : \\/:*?\"<>|");
+			$char = document_check_element_name(htmlspecialchars_decode(html_entity_decode($_['folder']), ENT_QUOTES));
+			if(!empty($char)) throw new Exception("Caractères interdits : ".$char);
 
 			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){
@@ -132,11 +121,10 @@ switch($_['action']){
 						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['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();
@@ -185,15 +173,8 @@ switch($_['action']){
 							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;
 
@@ -223,7 +204,6 @@ switch($_['action']){
 			$row['icon'] = $element->icon();
 			$row['childNumber'] = $element->childNumber;
 
-
 			$response['row'] = $row;
 		});
 	break;
@@ -237,8 +217,10 @@ switch($_['action']){
 			$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);
+			
+			$filePath = Element::root().$element->path;
+			$row['updatedLabel'] = date('d/m/Y H:i',filemtime($filePath));
+		
 			$bundle = base64_encode(json_encode(array(
 				'root' => $element->path,
 				'folder' => '',
@@ -255,7 +237,7 @@ switch($_['action']){
 		
 		$isopath =  Element::root().base64_decode(rawurldecode($_['path']));
 		$utf8Path = utf8_encode($isopath);
-		$osPath = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? $isopath : $utf8Path;
+		$osPath = get_OS() === 'WIN' ? $isopath : $utf8Path;
 		
 		$stream = Element::download($utf8Path);
 		$name = mt_basename($utf8Path);
@@ -269,7 +251,6 @@ switch($_['action']){
 		File::downloadStream($stream, $name, $mime);
 	break;
 
-
 	case 'document_element_move':
 		Action::write(function(&$response){
 			global $myUser,$_,$conf;
@@ -283,13 +264,18 @@ switch($_['action']){
 			$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'] = '';
+			/*
+				nb: cette ligne permet d'eviter d'ajouter un ./ devant le path de la bdd pour ce fichier lorsqu'il est déplacé
+				si le ./ est présent, la méthode browse (qui retourne le cehmin sans le ./) ne match pas avec la ligne en bdd et créé
+				une nouvelle ligne, ce qui rend le proprietaire du fichier anonymous
+			*/
+			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 : \\/:*?\"<>|");
+			$char = document_check_element_name(basename(htmlspecialchars_decode(html_entity_decode($to), ENT_QUOTES)));
+			if(!empty($char)) throw new Exception("Caractères interdits : ".$char);
 			$to .= SLASH.basename($from);
 
 			$element = Element::move($from,$to);
@@ -305,7 +291,7 @@ switch($_['action']){
 			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(!isset($_['label']) || empty($_['label'])) throw new Exception("Le nom ne doit pas être vide");
 
 			if(strlen($_['label']) > 80) throw new Exception("Taille maximale autorisée de 80 caractères.");
 			
@@ -315,20 +301,16 @@ switch($_['action']){
 			$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 se terminer par un '.'");
 
-			if(is_dir($fromOs) && substr($_['label'], -1,1)=='.') throw new Exception("Les dossiers ne peuvent pas terminer par un .");
-			
-
-			$to =  dirname($from).SLASH.$_['label'];
-
+			$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 : \\/:*?\"<>|");
+			$char = document_check_element_name(htmlspecialchars_decode(html_entity_decode($_['label']), ENT_QUOTES));
+			if(!empty($char)) throw new Exception("Caractères interdits : ".$char);
+			
 			$element = Element::move($from,$to);
 			
-			if(!$element) throw new Exception("Erreur lors de la récuperation de l'élement renommé", 500);
+			if(!$element) throw new Exception("Erreur lors de la récupération de l'élément renommé", 500);
 			
 			$element->path = str_replace('\\', '/', $element->path);
 			$response['element'] = $element;
@@ -341,8 +323,6 @@ switch($_['action']){
 			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']);
@@ -351,13 +331,11 @@ switch($_['action']){
 			Element::remove($path);
 
 			$extension = getExt($path);
-			if( in_array($extension, array('jpg','jpeg','png','gif','bmp'))) {
-
+			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;
@@ -371,18 +349,15 @@ switch($_['action']){
 
 			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.");
-			
+			if(!file_exists($osPath)) throw new Exception("Impossible de trouver le fichier, peut-être a t-il été supprimé entre temps, veuillez recharger la page.");
 			
 			$response['path'] = $path;
 			$response['label'] = mt_basename($path);
 			$response['content'] = Element::download($path);
-
+			if(in_array(getExt($path), array('html','htm'))) $response['wysiwyg'] = true;
 		});
 	break;
 
@@ -395,21 +370,19 @@ switch($_['action']){
 
 			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');
+			if($maxSize=='') $maxSize = 28060000;
 			$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;
 
@@ -423,9 +396,6 @@ switch($_['action']){
 			$response['sort'] = $_['sort'];
 
 			if(empty($_FILES)) throw new Exception("Aucun document à importer");
-			
-
-
 			$path = Element::root().$_['path'].SLASH;
 			$osPath = File::convert_decoding($path);
 
@@ -434,29 +404,16 @@ switch($_['action']){
 			$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');
-		
-
-			
+			if($conf->get('document_enable_logs')) Log::put("Upload d'un élément : ".$filePath,'document');			
 		});
 	break;
 
@@ -475,8 +432,6 @@ switch($_['action']){
 		});
 	break;
 
-
-
 	/** ELEMENTRIGHT **/
 	//Récuperation d'une liste de elementright
 	case 'document_right_search':

+ 2 - 2
plugin/document/app.json

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

+ 37 - 17
plugin/document/css/document.api.css

@@ -52,9 +52,11 @@
 	/*100vh - 50px (header) - 50px (footer)*/
 	height: calc(100vh - 100px);
 	overflow-y: auto;
-	resize: horizontal;
+	/*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;
@@ -378,9 +380,16 @@
 	position: relative;
 	z-index: 100;
 }
-
-
-.document-create-dropdown .dropdown-item.active,.document-create-dropdown .dropdown-item:active,
+.document-create-dropdown a.dropdown-item {
+	font-size: 0.9em;
+    padding: .25rem .75rem;
+    transition: background-color 0.05s linear;
+}
+.document-create-dropdown a.dropdown-item:hover {
+    background: #f8f8f8;
+}
+.document-create-dropdown .dropdown-item.active,
+.document-create-dropdown .dropdown-item:active,
 .document-create-dropdown .dropdown-item:hover{
 	background: inherit;
 	color: inherit;
@@ -475,7 +484,6 @@
 .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; 
@@ -719,7 +727,7 @@
 .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;
+	width: 160px;
 	font-size: 11px;
 }
 .file-module > table tbody tr {
@@ -781,31 +789,43 @@
 }
 .document-container .file-editor .file-editor-input{
 	width: 100%;
-	height:500px;
+	height:100%;
 	padding:15px;
 	color:#222222;
 	outline: none;
 }
-
-.document-container .btn-editor-save,.btn-editor-cancel{
+.document-container .btn-editor-save,.btn-editor-cancel,.btn-editor-expand{
 	border-radius: 0px;
-	float: right;
 }
-
 .document-container .file-editor .file-editor-header{
 	background: #f5f4f4;
+	display: flex;
+}
+.document-container .file-editor.expanded {
+    width: 100%;
+    position: fixed;
+    top: 50px;
+    left: 0;
+    height: 100%;
+}
+.document-container .file-editor.expanded .file-editor-header {
+    height: 38px;
+}
+.document-container .file-editor .file-editor-content {
+	/*header: 50px + editor: 38px = 88px*/
+    height:calc(100% - 88px);
 }
-
 .document-container .file-editor .file-editor-name{
+	flex: 1 0;
 	margin-left: 5px;
-	width:200px;
 	font-weight: bold;
 	height: 38px;
-	opacity:0.5;
-	transition: opacity 0.2s ease-in-out;
+	opacity: 0.5;
+	transition: opacity 0.1s ease-in-out;
 	outline: none;
 	background: transparent;
-	border:0;
+	border: 0;
+	z-index: 1100;
 }
 .document-container .file-editor .file-editor-name:hover{
 	opacity:1;
@@ -882,4 +902,4 @@
 	.footer{
 		display: none;
 	}
-}
+}

+ 37 - 13
plugin/document/document.plugin.php

@@ -27,7 +27,7 @@ function document_page(){
 
 //Fonction executée lors de l'activation du plugin
 function document_install($id){
-	if($id != 'fr.idleman.document') return;
+	if($id != 'fr.sys1.document') return;
 	Entity::install(__DIR__);
 	if(!file_exists(Element::root()))
 		mkdir(Element::root(),0755,true);
@@ -36,13 +36,13 @@ function document_install($id){
 		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_extensions','csv,xls,xlsx,doc,docx,dotm,dotx,pdf,png,jpg,jpeg,gif,tif,svg,bmp,txt,zip,gzip,msg');
 	$conf->put('document_allowed_size',10485760);
 }
 
 //Fonction executée lors de la désactivation du plugin
 function document_uninstall($id){
-	if($id != 'fr.idleman.document') return;
+	if($id != 'fr.sys1.document') return;
 	Entity::uninstall(__DIR__);
 }
 
@@ -79,7 +79,9 @@ function document_content_setting(){
 
 //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;
+	$pattern = "\/\\\:\*\?\"\<\>\|";
+	preg_match('|['.$pattern.']|i', $element, $match);
+	return !empty($match) ? $match[0] : '';
 }
 
 function document_dav_document($requested){
@@ -172,7 +174,7 @@ function document_dav_document($requested){
 		$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; 
+		$osPath = get_OS() === '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);
@@ -213,7 +215,9 @@ function document_dav_document($requested){
 		$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 : \\/:*?\"<>|");
+		$char = document_check_element_name(mt_basename($utf8To));
+		if(!empty($char)) throw new Exception("Caractères interdits : ".$char);
+
 		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');
 	};
@@ -223,7 +227,9 @@ function document_dav_document($requested){
 		$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 : \\/:*?\"<>|");
+		$char = document_check_element_name(mt_basename($utf8To));
+		if(!empty($char)) throw new Exception("Caractères interdits : ".$char);
+
 		Element::copy($utf8Path,$utf8To);
 		if($conf->get('document_enable_logs') || $conf->get('document_enable_logs_verbose')) Log::put('Copie de '.$utf8Path.' dans '.$utf8To,'document');
 	};
@@ -234,7 +240,7 @@ function document_dav_document($requested){
 		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; 
+		$osPath = get_OS() === 'WIN' ? $isoPath: $utf8Path; 
 		if(!file_exists($osPath)) throw new Exception("Not found", 404);
 
 		$files = array();
@@ -258,7 +264,7 @@ function document_dav_document($requested){
 		
 		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 );
+			$path = Element::root().(get_OS() === 'WIN' ? utf8_decode( $element->path) : $element->path );
 
 		    $file = array(
 				'type' => $element->type,
@@ -309,7 +315,6 @@ 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';
@@ -318,8 +323,10 @@ function document_widget(&$widgets){
 	$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';
+	if($myUser->can('document','configure')){
+		$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";
@@ -328,6 +335,20 @@ function document_widget(&$widgets){
 }
 
 
+function document_client_menu(&$clientMenu,$client){
+	$menu = new MenuItem();
+	$menu->label = 'Documents';
+	$menu->url = '#tab=document';
+	$menu->slug = 'document';
+	$menu->sort = 10;
+	$clientMenu[] = $menu;
+}
+
+function document_client_page($slug){
+	if($slug != 'document') return;
+	require_once(__DIR__.SLASH.'tab.client.php');
+}
+
 //Déclation des assets
 Plugin::addCss("/css/main.css"); 
 Plugin::addCss("/css/document.api.css"); 
@@ -346,4 +367,7 @@ 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");
-?>
+
+Plugin::addHook("client_menu", "document_client_menu");
+Plugin::addHook("client_page", "document_client_page");
+?>

File diff suppressed because it is too large
+ 289 - 436
plugin/document/js/document.api.js


+ 7 - 5
plugin/document/js/main.js

@@ -7,16 +7,18 @@ function init_plugin_document(){
 	}
 
 	var urlOptions = $.urlParam('data');
-	if(urlOptions && urlOptions!=''){
+	
+	if(urlOptions && urlOptions!='')
 		urlOptions = JSON.parse(atob(urlOptions));
-	}
 	
 	var doc = new DocumentApi('#document-container',urlOptions);
 
-	doc.load();
-
-
+	var data = $('#document-container').data();
+	if(!data.rightPermission) doc.options.rightPermission = false;
+	if(!data.rightDelete) doc.options.rightDelete = false;
+	if(!data.rightEdit) doc.options.rightEdit = false;
 
+	doc.load();
 }
 
 

+ 25 - 0
plugin/document/tab.client.php

@@ -0,0 +1,25 @@
+<?php
+global $_,$myUser;
+User::check_access('client','read');
+Plugin::need('client/Client');
+require_once(__DIR__.SLASH.'Element.class.php');
+$client = Client::getById($_['id']);
+
+$relativeDirectory = 'clients'.SLASH.$client->slug;
+$clientDirectory = Element::root().$relativeDirectory;
+if(!file_exists($clientDirectory)) mkdir($clientDirectory,0755,true);
+$element = Element::fromPath($clientDirectory);
+$element->author = $myUser->login;
+$element->save();
+
+?>
+
+<div class="p-2">
+	<div style="background-color: #ffffff;"  data-type="library" 
+	data-root-label="C:"
+	data-root="<?php echo $relativeDirectory;?>"  
+	data-tree-panel="false"  
+	data-search-panel="true"  
+	data-detail-panel="true" 
+	></div>
+</div>

+ 13 - 10
plugin/document/template.document.php

@@ -19,7 +19,7 @@
 	</div>
 
 	<div class="file-panel">
-		<div class="drag-overlay">
+		<div class="drag-overlay hidden">
 			<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>
@@ -37,9 +37,9 @@
 			
 
 			<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>
+				<button class="btn btn-small btn-primary dropdown-menu-button" 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="#">
+					<a class="dropdown-item {{lineClass}} hidden pointer" >
 						<div class="{{buttonClass}}"><i class='{{icon}}'></i> {{label}}</div>
 						{{{afterHtml}}}
 					</a>
@@ -56,12 +56,16 @@
 			<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="file-editor-header">
+					<i class="far fa-file-alt ml-3 my-auto"></i> <input type="text" class="file-editor-name" value="Nouveau fichier.txt">
+					<div class="btn btn-light btn-editor-expand" title="Agrandir"><i class="fas fa-expand-arrows-alt"></i></div>
 					<div class="btn btn-light btn-editor-cancel"><i class="fas fa-ban"></i> Annuler</div>
+					<div class="btn btn-primary btn-editor-save"><i class="far fa-check-circle"></i> Enregistrer</div>
 					<div class="clear"></div>
 				</div>
-				<textarea class="file-editor-input" placeholder="Aucun contenu pour le moment..."></textarea>
+				<div class="file-editor-content">
+					<textarea class="file-editor-input" placeholder="Aucun contenu pour le moment..."></textarea>
+				</div>
 			</div>
 
 			<!-- Vue liste -->
@@ -75,7 +79,7 @@
 					</tr>
 				</thead>
 				<tbody>
-					<tr class="hidden file-element" data-path="{{path}}" data-type="{{type}}" data-id="{{id}}" >
+					<tr class="hidden file-element" data-path="{{path}}" data-type="{{type}}" data-extension="{{extension}}" data-id="{{id}}" >
 						<td class="name-cell"><img  class="element-thumbnail" data-src="{{icon}}"/> 
 							<span>
 								<input type="text" class="rename-input hidden" value="">
@@ -90,7 +94,7 @@
 			</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}}">
+				<li class="file-element hidden  element-type-{{extension}}" data-path="{{path}}" data-type="{{type}}" data-extension="{{extension}}" data-id="{{id}}">
 					<div class="grid-container">
 						<div class="element-thumbnail" style="background-image:url({{thumbnail}});"></div>
 						<div  class="element-infos">
@@ -125,7 +129,7 @@
 
 
 	<!-- upload Preloader -->
-	<div class="preloader-upload-container">
+	<div class="preloader-upload-container hidden">
 		<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>
@@ -163,7 +167,6 @@
 			<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}}">

+ 5 - 6
plugin/document/widget.configure.php

@@ -1,6 +1,6 @@
 <?php 
-
 User::check_access('document','configure');
+
 $treepanel = $widget->data('widget-document-tree');
 $treepanel = $treepanel =="" ? 0 : $treepanel ;
 
@@ -15,9 +15,8 @@ $searchpanel = $searchpanel =="" ? 0 : $searchpanel ;
 	<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>
+		<label>Afficher le chemin relatif suivant<small class="text-muted">- eg : dossier1/sous dossier</small> :</label>
+		<input type="text" class="form-control form-control-sm" value="<?php echo $widget->data('widget-document-root'); ?>" id="widget-document-root" placeholder="dossier1/sous dossier">
+	</div>
 </div> 

+ 1 - 3
plugin/document/widget.php

@@ -1,9 +1,7 @@
 <?php 
 global $myUser;
 try{
-
-User::check_access('document','read');
-
+	User::check_access('document','read');
 ?>
 <div class="widgetDocumentContainer">
 	<div data-type="library" 

+ 7 - 7
plugin/example/Contact.class.php

@@ -1,7 +1,7 @@
 <?php
 
 class Contact extends Entity{
-	public $id,$label,$phone,$birth,$hour,$author,$address,$vehicle,$story,$login,$password,$icon,$mycheckbox1;
+	public $id,$label,$phone,$birth,$hour,$manager,$author,$address,$vehicle,$story,$storyShort,$login,$password,$icon,$mycheckbox1;
 	protected $TABLE_NAME = 'plugin_contact';
 	public $fields =
 	array(
@@ -10,10 +10,12 @@ class Contact extends Entity{
 		'phone' => 'string',
 		'birth' => 'date',
 		'hour' => 'date',
+		'manager'=> 'string',
 		'author' => 'string',
 		'address' => 'string',
 		'vehicle' => 'int',
 		'story' => 'longstring',
+		'storyShort' => 'longstring',
 		'login' => 'string',
 		'password' => 'string',
 		'icon'=>'string',
@@ -33,11 +35,9 @@ class Contact extends Entity{
 			'jpeg' => FILE_PATH.'contact'.SLASH.$this->id.'.jpeg',
 		);
 
-		$image['url'] = 'action.php?action=contact_download_picture&contact='.$this->id.'&extension=png';
+		$image['url'] = $image['path'] = 'img'.SLASH.'default-avatar.png';
 		foreach ($paths as $extension => $path) {
-			if (!file_exists( __ROOT__.$path) && !$finded) {
-				$image['path'] = __ROOT__.'img'.SLASH.'default-avatar.png';
-			} else if (!$finded) {
+			if (file_exists( __ROOT__.$path) && !$finded) {
 				$image['path'] = __ROOT__.$path;
 				$finded = true;
 				$image['url'] = 'action.php?action=contact_download_picture&contact='.$this->id.'&extension='.$extension;
@@ -49,7 +49,7 @@ class Contact extends Entity{
 	function documents(){
 		$documents = array();
 		foreach(glob(__ROOT__.FILE_PATH.'contact'.SLASH.'documents'.SLASH.$this->id.SLASH.'*.*') as $file){
-			if(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') $file = utf8_encode($file);
+			if(get_OS() === 'WIN') $file = utf8_encode($file);
 			$documents[] = array(
 				'path' => 'contact'.SLASH.'documents'.SLASH.$this->id.SLASH.basename($file),
 				'url' => 'action.php?action=contact_download_document&path='.$this->id.SLASH.rawurlencode(basename($file)),
@@ -60,4 +60,4 @@ class Contact extends Entity{
 		return $documents;
 	}
 }
-?>
+?>

+ 1 - 0
plugin/example/account.example.contact.php

@@ -0,0 +1 @@
+<div>Page de compte d'exemple contact</div>

+ 1 - 0
plugin/example/account.example.php

@@ -0,0 +1 @@
+<div>Page de compte d'exemple</div>

+ 135 - 104
plugin/example/action.php

@@ -15,12 +15,9 @@ switch($_['action']){
 			$contact = Contact::provide();
 			//on garde l'ancien objet a l'instant t pour le log comparatif (voir en fin d'action)
 			$oldcontact = clone $contact;
-			
-
-			$title = isset($contact->id) ? 'Édition d\'un contact' : 'Création d\'un contact';
-			$msg = isset($contact->id) ? 'Le contact '.$contact->label.' a été édité' : 'Création du contact '.$contact->label;
-
 			$contact->fromArray($_);
+			$contact->story = str_replace("&quot;", "'", $contact->story);
+			$contact->author = stripslashes($contact->author);
 			$contact->birth = timestamp_date($contact->birth);
 			$contact->hour = timestamp_hour($contact->hour);
 			$contact->save();
@@ -29,8 +26,8 @@ switch($_['action']){
 			if(!empty($_['document_temporary'])){
 				$files = json_decode($_['document_temporary'],true);
 				foreach($files as $file){
-					$from = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? File::temp().utf8_decode($file['path']) : File::temp().$file['path'];
-					$to = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($file['name']) : $file['name'];
+					$from = (get_OS() === 'WIN') ? File::temp().utf8_decode($file['path']) : File::temp().$file['path'];
+					$to = (get_OS() === 'WIN') ? utf8_decode($file['name']) : $file['name'];
 					File::move($from, 'contact'.SLASH.'documents'.SLASH.$contact->id.SLASH.$to);
 				}
 			}
@@ -38,15 +35,15 @@ switch($_['action']){
 			//Ajout de l'avatar a la base de media 
 			if(!empty($_FILES['avatar']) &&  $_FILES['avatar']['size']!=0 ){
 				
-				$logo = File::upload('avatar','contact'.SLASH.$contact->id.'.{{ext}}',1048576,array('jpg','png','jpeg'));
+				$logo = File::upload('avatar','contact'.SLASH.$contact->id.'.{{ext}}',10048576,array('jpg','png','jpeg'));
 				Image::resize($logo['absolute'],200,200);
 				Image::toJpg($logo['absolute']);
 			}
 
 			// GESTION ENVOI NOTIFICATION
 			Plugin::callHook('emit_notification',array(array(
-					'label' => $title,
-					'html' => $msg,
+					'label' => isset($contact->id) ? 'Édition d\'un contact' : 'Création d\'un contact',
+					'html' => isset($contact->id) ? 'Le contact '.$contact->label.' a été édité' : 'Création du contact '.$contact->label,
 					'type' => "notice",
 					'meta' => array('link' => ROOT_URL.'/index.php?module=example&page=sheet&id='.$contact->id),
 					'recipients' => array($myUser->login) // recipients contient login
@@ -66,122 +63,134 @@ switch($_['action']){
 
 	//Recherche d'une liste
 	case 'contact_search':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('example','read');
+			require_once(__DIR__.SLASH.'Contact.class.php');
 
-	Action::write(function(&$response){
-		global $myUser,$_;
-		User::check_access('example','read');
-		require_once(__DIR__.SLASH.'Contact.class.php');
-
-		$query = 'SELECT * FROM {{table}} WHERE 1';
-		$data = array();
-
-
+			$query = 'SELECT c1.* FROM {{table}} c1 WHERE 1';
+			$data = array();
 
-		//Recherche simple
-		if(!empty($_['filters']['keyword'])){
-			$query .= ' AND label LIKE ?';
-			$data[] = '%'.$_['filters']['keyword'].'%';
-		}
+			//Recherche simple
+			if(!empty($_['filters']['keyword'])){
+				$query .= ' AND label LIKE ?';
+				$data[] = '%'.$_['filters']['keyword'].'%';
+			}
 
-		//Recherche avancée
-		if(isset($_['filters']['advanced'])) filter_secure_query($_['filters']['advanced'],array('label','phone','birth','author','vehicle', 'mycheckbox1'),$query,$data);
+			//Recherche avancée
+			if(isset($_['filters']['advanced'])) filter_secure_query($_['filters']['advanced'],array('label','phone','birth','author','vehicle', 'mycheckbox1','login'),$query,$data);
 
-		//Tri des colonnes
-		if(isset($_['sort'])) sort_secure_query($_['sort'],array('label','phone'),$query,$data);
+			//Tri des colonnes
+			if(isset($_['sort'])) sort_secure_query($_['sort'],array('label','phone'),$query,$data);
 
-		//Pagination
-		$response['pagination'] = Contact::paginate(20,(!empty($_['page'])?$_['page']:0),$query,$data);
-		
-		//Mise en forme des résultats
-		foreach (Contact::staticQuery($query,$data,true) as $contact) {
-			$row = $contact->toArray(true);
-			$row['created'] = date('d/m/Y H:i',$contact->created);
-			$row['updated'] = date('d/m/Y H:i',$contact->updated);
+			//Pagination
+			$response['pagination'] = Contact::paginate(20,(!empty($_['page'])?$_['page']:0),$query,$data,'c1');
 			
-
-			$row['author'] = array();
-			foreach (explode(',',$contact->author) as $login) {
-				if(is_numeric($login)){
-					//rank
-					$item = Rank::getById($login);
-					$item = !$item ? new Rank(): $item;
-					$row['author'][] =$item->label;
-				}else{
-					//user
-					$row['author'][] = User::byLogin($login)->fullName();
+			$users = array();
+			foreach (User::getAll(false, false) as $user)
+				$users[$user->login] = $user->fullName();
+
+			//Mise en forme des résultats
+			foreach (Contact::staticQuery($query,$data,true) as $contact) {
+				$row = $contact->toArray(true);
+				$row['created'] = date('d/m/Y H:i',$contact->created);
+				$row['updated'] = date('d/m/Y H:i',$contact->updated);
+
+				$row['author'] = array();
+				foreach (explode(',',$contact->author) as $login) {
+					if(is_numeric($login)){
+						//rank
+						$item = Rank::getById($login);
+						$item = !$item ? new Rank(): $item;
+						$row['author'][] = $item->label;
+					}else{
+						//user
+						$row['author'][] = isset($users[$login]) ? $users[$login] : $login;
+					}
 				}
-				
-			}
-			$row['author'] = implode(',',$row['author']);
+				$row['author'] = implode(', ',$row['author']);
 
-			$row['birth'] = date('d/m/Y',$contact->birth);			
-			$row['picture'] = $contact->picture().'&v='.time();
-			$response['rows'][] = $row;
-		}
+				$row['birth'] = date('d/m/Y',$contact->birth);			
+				$row['picture'] = $contact->picture();
+				$response['rows'][] = $row;
+			}
 
-		/* Mode export */
-		if($_['export'] == 'true'){
-		    $stream = Excel::exportArray($response['rows'],null,'Export');
-		    File::downloadStream($stream,'export-'.date('d-m-Y').'.xlsx');
-		    exit();
-		}
-	});
+			/* Mode export */
+			if($_['export'] == 'true'){
+			    $stream = Excel::exportArray($response['rows'],null,'Export');
+			    File::downloadStream($stream,'export-'.date('d-m-Y').'.xlsx');
+			    exit();
+			}
+		});
 	break;
 
 	//Suppression par id
 	case 'contact_delete':
-	Action::write(function(&$response){
-		global $myUser,$_;
-		User::check_access('example','delete');
-		require_once(__DIR__.SLASH.'Contact.class.php');
-		if(!isset($_['id']) || !is_numeric($_['id'])) throw new Exception("Id non spécifié ou non numerique");
-		
-		//Exemple de mise en place de logs comparatif
-		Log::compare(Contact::getById($_['id']),false);
-		//suppression 
-		Contact::deleteById($_['id']);
-	});
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('example','delete');
+			require_once(__DIR__.SLASH.'Contact.class.php');
+			if(!isset($_['id']) || !is_numeric($_['id'])) throw new Exception("Id non spécifié ou non numerique");
+			
+			//Exemple de mise en place de logs comparatif
+			Log::compare(Contact::getById($_['id']),false);
+			//suppression 
+			Contact::deleteById($_['id']);
+		});
+	break;
+
+	//Création rapide par quickform
+	case 'contact_quick_create':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('example','edit');
+			require_once(__DIR__.SLASH.'Contact.class.php');
+			
+			ob_start();
+			require_once(__DIR__.SLASH.'page.quick.example.php');
+			$response['content'] = ob_get_clean();
+		});
 	break;
 
 	//Suppression document
 	case 'contact_delete_document':
-	Action::write(function(&$response){
-		global $myUser,$_;
-		User::check_access('example','delete');
-		require_once(__DIR__.SLASH.'Contact.class.php');
-		if(!isset($_['path']) ) throw new Exception("Chemin non spécifié ou non numerique");
-		//Le premier argument est un namspace de sécurité 
-		//et assure que le fichier sera toujours cloisoné dans un contexte file/contact/documents
-		$path = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($_['path']) : $_['path'];
-		File::delete('contact'.SLASH.'documents',$path);
-	});
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('example','delete');
+			require_once(__DIR__.SLASH.'Contact.class.php');
+			if(!isset($_['path']) ) throw new Exception("Chemin non spécifié ou non numerique");
+			//Le premier argument est un namspace de sécurité 
+			//et assure que le fichier sera toujours cloisoné dans un contexte file/contact/documents
+			$path = (get_OS() === 'WIN') ? utf8_decode($_['path']) : $_['path'];
+			File::delete('contact'.SLASH.'documents',$path);
+		});
 	break;
 
 	case 'contact_add_document':
-	Action::write(function(&$response){
-		global $myUser,$_;
-		User::check_access('example','edit');
-		require_once(__DIR__.SLASH.'Contact.class.php');
-
-		$contact = Contact::provide();
-		$contact->save();
-
-		foreach ($_['files'] as $file) {
-			$name = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($file['name']) : $file['name'];
-			$row = File::move(File::temp().$file['path'],'contact'.SLASH.'documents'.SLASH.$contact->id.SLASH.$name);
-			$row['url'] = 'action.php?action=contact_download_document&path='.SLASH.$contact->id.SLASH.rawurlencode($file['name']);
-			$row['oldPath'] = $file['path'];
-			$response['files'][] = $row;
-		}
-		$response['id'] = $contact->id;
-	});
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('example','edit');
+			require_once(__DIR__.SLASH.'Contact.class.php');
+
+			$contact = Contact::provide();
+			$contact->save();
+
+			foreach ($_['files'] as $file) {
+				$name = (get_OS() === 'WIN') ? utf8_decode($file['name']) : $file['name'];
+				$row = File::move(File::temp().$file['path'],'contact'.SLASH.'documents'.SLASH.$contact->id.SLASH.$name);
+				$row['url'] = 'action.php?action=contact_download_document&path='.SLASH.$contact->id.SLASH.rawurlencode($file['name']);
+				$row['oldPath'] = $file['path'];
+				$response['files'][] = $row;
+			}
+			$response['id'] = $contact->id;
+		});
 	break;
 
 	//Téléchargement des documents
 	case 'contact_download_document':
 		global $myUser,$_;
 		User::check_access('example','read');
-		$path = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($_['path']) : $_['path'];
+		$path = (get_OS() === 'WIN') ? utf8_decode($_['path']) : $_['path'];
 		File::downloadFile(File::dir().'contact'.SLASH.'documents'.SLASH.$path);
 	break;
 
@@ -230,10 +239,34 @@ switch($_['action']){
 			$response['content'] = $stream;
 		});
 	break;
+
+	case 'example_setting_save':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('example','configure');
+
+			foreach(Configuration::setting('example') as $key=>$value){
+				if(!is_array($value)) continue;
+				$allowed[] = $key;
+			}
+			foreach ($_['fields'] as $key => $value)
+				if(in_array($key, $allowed)) $conf->put($key,$value);
+
+			//Ajout des fichiers joints
+			if(isset($_['fields']['document_temporary']) && !empty($_['fields']['document_temporary'])){
+				$files = json_decode($_['fields']['document_temporary'],true);
+				foreach($files as $file){
+					$from = (get_OS() === 'WIN') ? File::temp().utf8_decode($file['path']) : File::temp().$file['path'];
+					$to = (get_OS() === 'WIN') ? utf8_decode($file['name']) : $file['name'];
+					File::move($from, 'contact'.SLASH.'documents'.SLASH.'settings'.SLASH.$to);
+				}
+			}
+		});
+	break;
 	
 	default : 
 		global $myFirm;
-		if($myFirm->has_plugin('fr.idleman.stripe') && $_['action']=='example_stripe_pay'){
+		if($myFirm->has_plugin('fr.sys1.stripe') && $_['action']=='example_stripe_pay'){
 			Action::write(function(&$response){
 				global $_;
 				//paye la somme de 20 €
@@ -242,6 +275,4 @@ switch($_['action']){
 		}
 	break;
 }
-
-
-?>
+?>

+ 2 - 2
plugin/example/app.json

@@ -1,11 +1,11 @@
 {
-	"id": "fr.idleman.example",
+	"id": "fr.sys1.example",
 	"name": "Example",
 	"author" : {
 		"name" : ""
 	},
 	"version": "1.0",
-	"url": "http://idleman.fr",
+	"url": "http://sys1.fr",
 	"licence": {"name": "Copyright","url" : ""},
 	"description": "Contient les différents outils créés et la structure de fichier attendue dans les plugins.",
 	"require" : {}

+ 97 - 52
plugin/example/example.plugin.php

@@ -18,6 +18,7 @@ function example_page(){
 	global $_,$myUser;
 	if(!isset($_['module']) || $_['module'] !='example') return;
 	$page = !isset($_['page']) ? 'list' : $_['page'];
+	$page = str_replace('..','',$page);
 	$file = __DIR__.SLASH.'page.'.$page.'.php';
 	if(!file_exists($file)) throw new Exception("Page ".$page." inexistante");
 	require_once($file);
@@ -25,14 +26,13 @@ function example_page(){
 
 //Fonction executée lors de l'activation du plugin
 function example_install($id){
-	if($id != 'fr.idleman.example') return;
+	if($id != 'fr.sys1.example') return;
 	Entity::install(__DIR__);
-
 }
 
 //Fonction executée lors de la désactivation du plugin
 function example_uninstall($id){
-	if($id != 'fr.idleman.example') return;
+	if($id != 'fr.sys1.example') return;
 	Entity::uninstall(__DIR__);
 }
 
@@ -68,10 +68,58 @@ function example_content_setting(){
 		require_once(__DIR__.SLASH.'setting.'.$_['section'].'.php');
 }
 
-//Déclation des assets
+function example_menu_account(&$accountMenu){
+	global $_, $myUser;
+	if(!$myUser->can('example', 'read')) return;
+	$accountMenu[]= array(
+		'sort' =>0,
+		'url' => 'account.php?section=example',
+		'icon' => 'fas fa-angle-right',
+		'label' => 'Example',
+	);	
+	$accountMenu[]= array(
+		'sort' =>0,
+		'url' => 'account.php?section=example.contact',
+		'icon' => 'fas fa-angle-right',
+		'label' => 'Example Contact',
+	);	
+}
+//Déclaration des pages de réglages
+function example_content_account(){
+	global $_;
+	if(file_exists(__DIR__.SLASH.'account.'.$_['section'].'.php'))
+		require_once(__DIR__.SLASH.'account.'.$_['section'].'.php');
+}
+
+
+function example_document_setting(){
+	$documents = array();
+	foreach(glob(__ROOT__.FILE_PATH.'contact'.SLASH.'documents'.SLASH.'settings'.SLASH.'*.*') as $file){
+		if(get_OS() === 'WIN') $file = utf8_encode($file);
+		$documents[] = array(
+			'path' => 'contact'.SLASH.'documents'.SLASH.'settings'.SLASH.basename($file),
+			'url' => 'action.php?action=contact_download_document&path=settings'.SLASH.rawurlencode(basename($file)),
+			'name' => basename($file),
+			'icon' => getExtIcon(getExt($file))
+		);
+	}
+	return $documents;
+}
+
 
+Configuration::setting('example',array(
+    "Général",
+    'example_dropzone' => array("label"=>"Dropzone de fichiers","type"=>"dropzone","legend"=>"la légende","parameters"=>array(
+    	"data-allowed" => "docx,pdf,txt,jpg,bmp,gif,xlsx,png,iso",
+    	"data-label" => "Faites glisser vos documents",
+    	"data-delete" => "contact_delete_document",
+    	"documents" => json_encode(example_document_setting())
+    )),
+));
 
-Plugin::addJs("/js/document.api.js");
+
+
+//Déclation des assets
 Plugin::addCss("/css/main.css"); 
 Plugin::addJs("/js/main.js");
 
@@ -81,70 +129,67 @@ Plugin::addHook("uninstall", "example_uninstall");
 Plugin::addHook("section", "example_section");
 Plugin::addHook("menu_main", "example_menu"); 
 Plugin::addHook("page", "example_page");  
-Plugin::addHook("action", "example_action");  
+Plugin::addHook("action", "example_action"); 
+
 Plugin::addHook("menu_setting", "example_menu_setting");    
 Plugin::addHook("content_setting", "example_content_setting");    
 
+Plugin::addHook("menu_account", "example_menu_account");
+Plugin::addHook("content_account", "example_content_account");
 
 
 global $myFirm;
-if($myFirm->has_plugin('fr.idleman.export')){
+if($myFirm->has_plugin('fr.sys1.export')){
 	require_once(__ROOT__.PLUGIN_PATH.'export'.SLASH.'ExportModel.class.php');
 
-	ExportModel::add('example','contact-sheet', 'Fiche contact', function($params){
+	ExportModel::add('example','contact-sheet', 'Fiche contact', function($parameters){
 		global $myUser;
 		
-		if(isset($params['description']) && $params['description']!=true){
-			require_once(__DIR__.SLASH.'Contact.class.php');
-			$contact = Contact::getById($params['id']);
-
-			$data['contact.libellé'] = $contact->label;
-			$data['contact.téléphone'] = $contact->phone;
-			$data['contact.anniversaire'] = date('d/m/Y',$contact->birth);
-			$data['contact.identifiant'] = $contact->login;
-			//Ajout du champs spécial de photo de contact
-			$data['contact.photo'] = '::'.$contact->get_image('path'); 
-
-		} else {
-			$data['contact.libellé'] = 'Libellé du contact';
-			$data['contact.téléphone'] = 'N° de téléphone du contact';
-			$data['contact.anniversaire'] = 'Date de naissance/d\'anniversaire du contact';
-			$data['contact.identifiant'] = 'Identifiant du contact';
-			$data['contact.photo'] = array('desc'=>'Image de profil du contact', 'type'=>'photo');
-		}
+		require_once(__DIR__.SLASH.'Contact.class.php');
+
+		$contact = new Contact();
 
+		if(isset($parameters['description']) && $parameters['description']!=true && !empty($parameters['id']))
+			$contact =  Contact::getById($parameters['id']);
+		
+		
+		$data['contact'] = array('label'=>'Contact', 'type'=>'object','value' => array());
+		$data['contact']['value']['libellé'] = array('label' => 'Libellé du contact','value' => $contact->label);
+		$data['contact']['value']['téléphone'] = array('label' => 'N° de téléphone du contact','value' => $contact->phone);
+		$data['contact']['value']['anniversaire'] = array('label' => 'Date de naissance/d\'anniversaire du contact','value' => date('d/m/Y',$contact->birth));
+		$data['contact']['value']['identifiant'] = array('label' => 'Identifiant du contact','value'=>$contact->login);
+		$data['contact']['value']['photo'] = array(
+			'label'=>'Image de profil du contact',
+			'type'=>'image',
+			'value' => file_get_contents($contact->get_image('path'))
+		);
 		return $data;
 	});
 
-	ExportModel::add('example','contact-list','Liste de contacts',function($params){
+	ExportModel::add('example','contact-list','Liste de contacts',function($parameters){
 		global $myUser;
-		if(isset($params['description']) && $params['description']!=true){
-			require_once(__DIR__.SLASH.'Contact.class.php');
-
-			foreach(Contact::loadAll() as $contact){
-				//définition jeu de données
-				$row['contact.libellé'] = $contact->label;
-				$row['contact.téléphone'] = $contact->phone;
-				$row['contact.anniversaire'] = date('d/m/Y',$contact->birth);
-				$row['contact.identifiant'] = $contact->login;
-
-				//Ajout du champs spécial de photo de contact
-				$row['contact.photo'] = '::'.$contact->get_image('path'); 
-				
-
-				$data['liste.contacts'][] = $row;
-			}
-		} else {
-			$data['liste.contacts.nombre'] ='Nombre de contacts au total';
-			$subItems = array(
-				'contact.libellé' => 'Libellé du contact',
-				'contact.téléphone' => 'N° de téléphone du contact',
-				'contact.anniversaire' => 'Date de naissance/d\'anniversaire du contact',
-				'contact.identifiant' => 'Identifiant du contact',
-				'contact.photo' => array('desc'=>'Image de profil du contact', 'type'=>'photo'),
+		require_once(__DIR__.SLASH.'Contact.class.php');
+
+		$contacts = array(new Contact());
+		if(isset($parameters['description']) && $parameters['description']!=true) $contacts = Contact::loadAll();
+		
+		$data['contacts'] = array('label'=>'Boucle sur les contacts', 'type'=>'list','value' => array());
+		$data['contacts']['nombre'] = array('label' => 'Nombre de contacts au total', 'value' => count($contacts));
+		foreach($contacts as $contact){
+
+		$data['contacts']['value'][] = array(
+				'libellé' => array('label' => 'Libellé du contact' , 'value' => $contact->label ),
+				'téléphone' => array('label' => 'N° de téléphone du contact' , 'value' => $contact->phone ),
+				'anniversaire' => array('label' => 'Date de naissance/d\'anniversaire du contact' , 'value' => $contact->birth ),
+				'identifiant' => array('label' => 'Identifiant du contact' , 'value' => $contact->login ),
+				'photo' => array(
+					'label' => 'Image de profil du contact', 
+					'type'=>'image' , 
+					'value' => file_get_contents($contact->get_image('path'))
+				),
 			);
-			$data['liste.contacts'] = array('desc'=>'Boucle sur les contacts', 'type'=>'list', 'subitems'=>$subItems);
 		}
+
 		return $data;
 	});
 }

+ 43 - 24
plugin/example/js/main.js

@@ -4,42 +4,31 @@ function init_plugin_example(){
 		default:
 		break;
 	}
-
-
 	$('#contacts').sortable_table({
 		onSort : contact_search
 	});
 }
 
-function init_setting_example(){
-	
-	var doc = new DocumentApi('#example-doc');
-
-	doc.load();
-
-
-
-}
-
 /**
- * 
+ *
  * QUICKFORM
- * 
+ *
  */
 function example_quickform_buttons(){
 	$('.quickform-modal .modal-footer').append('<div class="btn btn-success" onclick="contact_save(contact_submit_quickform);"><i class="fas fa-check"></i> Ajouter</div>');
 }
 // Callback du quickform on save
 function contact_submit_quickform(){
-	//do something
 	$('#quickform-modal').modal('hide');
 }
 
 //GESTION CONTACT
 function contact_search(callback, exportMode){
+	var box = new FilterBox('#filters');
+	
 	$('#contacts').fill({
 		action:"contact_search",
-		filters : $('#filters').filters(),
+		filters :  box.filters(),
 		//Gestion du tri par colonnes de tableau (optionnel)
 		sort : $('#contacts').sortable_table('get'),
 		//Activation de l'export excel (optionnel)
@@ -56,7 +45,7 @@ function contact_search(callback, exportMode){
 					transform:'translateX(0px)',
 					opacity : 1
 				})
-			},(i+1)*10);
+			},(i+1)*5);
 		}
 	},function(response){
 		$('.results-count span').text(response.pagination.total);
@@ -64,7 +53,7 @@ function contact_search(callback, exportMode){
 	});
 }
 
-//Sauvegarde 
+//Sauvegarde
 function contact_save(cb){
 	var data = $('#contactForm').toJson();
 	data.id = $('#contactForm').attr('data-id');
@@ -72,12 +61,12 @@ function contact_save(cb){
 	$.action(data,function(r){
 		if(cb) cb();
 		$('#contactForm').attr('data-id',r.id);
-		
+
 		$.message('success','Enregistré');
 	});
 }
 
-//Suppression 
+//Suppression
 function contact_delete(element){
 	if(!confirm('Êtes vous sûr de vouloir supprimer cet item ?')) return;
 	var line = $(element).closest('tr');
@@ -122,7 +111,7 @@ function contact_add_document(files){
 			line.find('i.pointer').attr('onclick', 'contact_delete_document(this)');
 
 			$('[data-type="dropzone"] input:not(:visible)', form).val('');
-			
+
 			$.message('success', 'Fichier "'+file.name+'" sauvegardé');
 		});
 
@@ -133,7 +122,7 @@ function contact_add_document(files){
 function contact_avatar_delete(element){
 	if(!confirm('Êtes vous sûr de vouloir supprimer l\'image ?')) return;
 	var imageComposer = $(element).parent().find("input[data-type='image']");
-	
+
 	$.action({
 		action: 'contact_avatar_delete',
 		id: $('#contactForm').attr('data-id')
@@ -146,11 +135,41 @@ function contact_avatar_delete(element){
 }
 
 /* EXPORT MODELE */
-function contact_export_callback(){
+function contact_export_pre_callback(){
 	console.log('Callback custom après chargement du modal');
-	
+
 	setTimeout(function(){
 		$('#export-modal .cb-custom-btn').remove();
 		$('#export-modal .modal-footer').prepend('<div class="btn btn-primary mr-auto cb-custom-btn"><i class="fas fa-check"></i> Bouton ajouté avec le callback custom</div>');
 	},0);
+}
+
+function contact_export_post_callback(){
+	alert('Callback custom après export');
+}
+
+/* EXAMPLE LOCATION CALLBACK */
+function example_location_select(location){
+	var attributes = {};
+	for(var key in location){
+	    attributes['data-'+key] = location[key];
+	    if($('#'+key)!='') $('#'+key).val(location[key]).text(location[key]);
+	}
+}
+
+function example_location_geocode(infos){
+	alert("Latitude: "+infos.Response.View[0].Result[0].Location.DisplayPosition.Latitude+"\nLongitude: "+infos.Response.View[0].Result[0].Location.DisplayPosition.Longitude);
+}
+
+
+
+/** SETTINGS */
+//Enregistrement des configurations
+function example_setting_save(){
+	$.action({ 
+		action : 'example_setting_save', 
+		fields :  $('#example-setting-form').toJson() 
+	}, function(r){
+		$.message('success','Enregistré');
+	});
 }

+ 17 - 11
plugin/example/page.list.php

@@ -3,26 +3,33 @@ global $myFirm;
 User::check_access('example','read');
 
 require_once(__DIR__.SLASH.'Contact.class.php');
+
+$defaultFilters = filters_default(array(
+    "jean",
+    array(
+        'birth' => "17/09/1998",
+        'join' => 'or'
+    ),
+    array(
+        'phone:like' => "9754"
+    )
+));
 ?>
 
 <div class="row">
 
     <div class="col-md-8">
         <!-- 
-            data-join : spécifie la liaison par défaut des filtres (and | or) si rien n'est spéficié ou que l'attrbute n'existe pas, un select apparait pour
-                        que l'utilisateur puisse choisir
-            data-slug : si spécifié, la recherche devient enregistrable pour une réutilisation ultérieure
-            data-only-advanced :si l'attribut est présent,  cache la recherche simple et ouvre par defaut la recherche avancée
-            data-autosearch (default: true) : si définit a false, ne lancera pas la fonction data-function automatiquement en fin de chargement du composant
+            voir filter.component.js pour la documentation
         -->
-        <select id="filters"  data-slug="contact-search" data-type="filter" data-label="Recherche" data-join="and" data-function="contact_search" >
+        <select id="filters" data-type="filter" data-slug="contact-search"  data-default='<?php echo json_encode($defaultFilters); ?>' data-label="Trouver un contact" data-function="contact_search">
             <!-- pour une recherche simple, ne pas spécifier d'options dans cette liste -->
             <option value="birth" data-filter-type="date">Date de naissance</option>
             <option value="label" data-filter-type="text">Libellé</option>
             <option value="author" data-filter-type="user">Auteur</option>
             <option value="phone"   data-filter-type="text">Téléphone</option>
-            <option value="vehicle" data-filter-type="dictionnary" data-slug="vehicles" data-depth="2" data-disable-label>Véhicule</option>
-            <option value="customList" data-filter-type="dictionnary" data-filter-source='<?php echo json_encode(array('c1'=>'Valeur 1','c2'=>'valeur 2')); ?>'>Ma liste custom</option>
+            <option value="vehicle" data-filter-type="dictionnary" data-slug="vehicles" data-depth="3" data-disable-label>Véhicule</option>
+            <option value="login" data-filter-type="dictionnary" data-filter-source='<?php echo json_encode(array('nigol'=>'Nigol','pouet'=>'Pouet')); ?>'>Ma liste custom</option>
             <option value="myNumber"  data-filter-type="number">Un nombre</option>
             <option value="mycheckbox1"  data-filter-type="boolean">Ma checkbox 1</option>
         </select>
@@ -33,14 +40,13 @@ require_once(__DIR__.SLASH.'Contact.class.php');
 		<a href="index.php?module=example&page=sheet" class="btn btn-success right"><i class="fas fa-plus"></i> Ajouter un contact</a>
 		<?php endif; ?>
         
-        <?php if($myUser->can('export', 'read') && $myFirm->has_plugin('fr.idleman.export')) : ?>
-        <div class="right mr-2 d-inline-block" data-type="export-model" data-default="testouille" data-callback="contact_export_callback" data-parameters='<?php echo stripslashes(json_encode(array("plugin"=>"example","dataset"=>"contact-list"))); ?>'>
+        <?php if($myUser->can('export', 'read') && $myFirm->has_plugin('fr.sys1.export')) : ?>
+        <div class="right mr-2 d-inline-block" data-type="export-model" data-default="xlsx2" data-pre-callback="contact_export_pre_callback" data-post-callback="contact_export_post_callback" data-parameters='<?php echo stripslashes(json_encode(array("plugin"=>"example","dataset"=>"contact-list"))); ?>'>
             <div class="btn btn-primary"><i class="far fa-file"></i> Export modèle</div>
         </div>
         <?php endif; ?>
 	</div>
 </div>
-<br/>
 <h4 class="results-count"><span></span> Résultat(s) <div class="btn btn-dark btn-small" onclick="contact_search(null,true)"><i class="far fa-file-excel"></i> Exporter</div></h4>
 <div class="row">
 	<div class="col-xl-12">

+ 1 - 1
plugin/example/page.quick.example.php

@@ -1,7 +1,7 @@
 <form id="example-form" data-id="" data-action="contact_save" class="quickform-example-form">
 	<div class="form-group">
 		<label for="label"  class="form-control-label font-weight-bold">Libellé : </label>
-		<input required type="text" value="" placeholder="Raison sociale ou nom" class="form-control" id="label" name="label"/>
+		<input required type="text" value="<?php echo $myUser->login ?>" placeholder="Raison sociale ou nom" class="form-control" id="label" name="label"/>
 	</div>
 	<div class="form-group">
 		<label for="phone"><strong>Téléphone : </strong></label>

+ 52 - 21
plugin/example/page.sheet.php

@@ -4,12 +4,19 @@ User::check_access('example','read');
 require_once(__DIR__.SLASH.'Contact.class.php');
 $contact = Contact::provide();
 
+$data = array(
+	'subject' => (!empty($contact->label) ? 'Envoi de la fiche contact de '.$contact->label : 'Envoi fiche contact'),
+	'message' => "Contenu du message envoyé",
+	'recipients' => array('to'=>array('claude@claude.fr', 'testouille@test.fr'), 'cc'=>array('carboncopy@test.net'), 'cci'=>array('another@email.fr')),
+	'attachments' => $contact->documents()
+);
+
 ?>
 <div class="container-small">
 	<br>
 	<label for="label">Composant Card</label>
 	<small class="text-muted"> (Span data-type="card")</small>
-	<h3>Fiche du contact <span data-type="card" data-show-delay="800" data-action="example_contact_card" data-parameters='<?php echo json_encode(array('id'=>$contact->id)); ?>'><?php echo html_decode_utf8($contact->label); ?></span></h3>
+	<h3>Fiche du contact <span data-type="card" data-show-delay="200" data-action="example_contact_card" data-parameters='<?php echo json_encode(array('id'=>$contact->id)); ?>'><?php echo html_decode_utf8($contact->label); ?></span></h3>
 	<br>
 	<form class="row" method="POST" enctype="multipart/form-data" id="contactForm" data-action="contact_save" data-id="<?php echo $contact->id; ?>">
 
@@ -30,8 +37,23 @@ $contact = Contact::provide();
 			<small class="text-muted"> (Input data-type="hour")</small>
 			<input type="text" data-type="hour" value="<?php echo $contact->hour!=0?date('H:i',$contact->hour):''; ?>" data-step="20" placeholder="L'heure de naissance" class="form-control" id="hour" name="hour"/>
 			<br>
+			<label for="manager">Manager</label>
+			<small class="text-muted" data-placement="right" data-tooltip title="* data-multiple			:	Autorise l'utilisateur a sélectionner  des entité multiples 
+			* data-types (default: user) : définis les entité sélectionnables (ex: user,rank)	"> (Input data-type="user")</small>
+			
+
+			<div class="input-group mb-3">
+				<input type="text" data-type="user" data-types="user" value="<?php echo $contact->manager; ?>" placeholder="Manager du contact" class="form-control" id="manager" name="manager"/>
+				<input type="text" data-type="color" value="#55e6c1" onchange="$('#mainMenu').css('backgroundColor',$(this).val())" class="form-control" id="color" name="color"/>
+			</div>
+
+			<label for="color">Couleur</label>
+			<small class="text-muted"> (Input data-type="color")</small>
+			<input type="text" data-type="color" value="pink" class="form-control" id="color" name="color"  onchange="$('#mainMenu').css('backgroundColor',$(this).val())"/>
+			<br>
 			<label for="author">Auteur</label>
-			<small class="text-muted" data-placement="right" data-tooltip="* data-multiple			:	Autorise l'utilisateur a sélectionner  des entité multiples" data-tooltip="* data-types (default: user) : définis les entité sélectionnables (ex: user,rank)	"> (Input data-type="user")</small>
+			<small class="text-muted" data-placement="right" data-tooltip title="* data-multiple			:	Autorise l'utilisateur a sélectionner  des entité multiples
+			* data-types (default: user) : définis les entité sélectionnables (ex: user,rank)	"> (Input data-type="user")</small>
 			<input type="text" data-type="user" data-multiple data-types="user,rank" value="<?php echo $contact->author; ?>" placeholder="Auteur du contact" class="form-control" id="author" name="author"/>
 			<br>
 			<label for="picture">Image</label>
@@ -44,7 +66,7 @@ $contact = Contact::provide();
             <input id="icon" data-type="icon" name="icon" class="form-control" placeholder="Icône du contact" value="<?php echo !empty($contact->icon) ? $contact->icon : 'fab fa-btc'; ?>" type="text"></span>
 			<br>
 			<label for="vehicle">Véhicule</label>
-			<small class="text-muted" data-placement="right" data-tooltip="* data-depth 			:	nb de profondeur de liste
+			<small class="text-muted" data-placement="right" data-tooltip title="* data-depth 			:	nb de profondeur de liste
 * data-slug 			:	le slug de la liste mère à afficher
 * data-value 			:	la valeur de l'entité  à récup en base
 * data-disable-label 	:	cache le label de sous-liste si mentionné">(Select data-type="dictionnary")</small>
@@ -52,12 +74,12 @@ $contact = Contact::provide();
 
 
 			<label for="vehicle2">Véhicule (tags)</label>
-			<small class="text-muted" data-placement="right" data-tooltip="* data-multiple			:	Autorise l'utilisateur a sélectionner  des tag multiples
+			<small class="text-muted" data-placement="right" data-tooltip title="* data-multiple			:	Autorise l'utilisateur a sélectionner  des tag multiples
 * data-slug 			:	le slug de la liste mère à afficher">(Select data-type="tag-list")</small>
 			<input type="text" data-type="tag-list" data-slug="vehicles" class="form-control" />
 
 			<label for="vehicle2">Catégories</label>
-			<small class="text-muted" data-placement="right" data-tooltip="* data-tags	(json)	:tags disponibles (eg. 
+			<small class="text-muted" data-placement="right" data-tooltip title="* data-tags	(json)	:tags disponibles (eg. 
 
 			{
 				'bug':{'icon':'fas fa-bug','label':'Bug','color':'#cb2431'},
@@ -68,42 +90,49 @@ $contact = Contact::provide();
 			<input type="text" data-type="tagcloud" data-tags='{"bug":{"icon":"fas fa-bug","label":"Bug","color":"#cb2431"},"feature":{"icon":"fas fa-check","label":"Demande","color":"#2cbe4e"},"question":{"icon":"far fa-question-circle","label":"Question","color":"#00BCD4"}}' class="form-control"  value="bug,question"/>
 
 
-			<label for="country">Pays</label>
+			<label for="example-country">Pays</label>
 			<small class="text-muted">(Select data-type="dictionnary")</small>
-			<select data-type="dictionnary" data-slug="countries" data-depth="3" data-id="country" data-value="" class="form-control select-control" name="country" id="country"></select>
+			<select data-type="dictionnary" data-slug="countries" data-depth="3" data-id="country" data-value="" class="form-control select-control" name="example-country" id="example-country"></select>
 			<br>
 
 
 			<label for="country">Etat</label>
-			<small data-tooltip="* data-icon : l'icone affichée près du label (optionnel)" class="text-muted">(Select data-type="dropdown-select")</small><br/>
+			<small data-tooltip title="* data-icon : l'icone affichée près du label (optionnel)" class="text-muted">(Select data-type="dropdown-select")</small><br/>
 			<select data-type="dropdown-select" id="state">
             	<option selected="selected" value="active" style="background-color:#2cbe4e;color:#ffffff;" data-icon="far fa-check-circle">Actif</option>
             	<option value="inactive" style="background-color:#c90000;color:#ffffff;" data-icon="far fa-times-circle">Inactif</option>
             </select><br/>
 
 
-
 			<label for="address">Adresse</label>
 			<small class="text-muted"> (Input data-type="location")</small>
-			<input type="text" data-type="location" data-input-city="#city"  data-input-latitude="#latitude"  value="<?php echo $contact->address; ?>" class="form-control" id="address" name="address"/>
+			<input type="text" data-type="location" data-filter-geocode data-select-callback="example_location_select" data-geocode-callback="example_location_geocode" data-filter-country="FRA" value="<?php echo $contact->address; ?>" class="form-control" id="address" name="address"/>
 			Ville : <span id="city">...</span><br>
-			Latitude : <span id="latitude">...</span>
+			Pays : <span id="country">...</span>
 			<br><br>
 			<label for="document">Documents</label>
-			<small class="text-muted" data-placement="right" data-tooltip="* data-label 			:	le label affiché dans la zone
+			<small class="text-muted" data-placement="right" data-tooltip title="* data-label 			:	le label affiché dans la zone
 * data-delete 			:	méthode de suppression de doc de l'entité 
+* data-max-files 	:	nombre de fichiers uploadables (defaut : 0 pour infini) 
 * data-preview 		:	Affiche l'apercu de l'image si possible
 * data-readonly		:       false/true Active désactive la possibilité d'envoyer du fichier
 * data-allowed		:	 les extensions de fichier acceptées"> (Div data-type="dropzone")</small>
-			<div data-type="dropzone"  data-label="Faites glisser vos documents ici" data-delete="contact_delete_document" data-save="contact_add_document" data-allowed="docx,pdf,txt,jpg,bmp,gif,xlsx,png,iso" class="form-control" id="document" name="document">
+			<div data-type="dropzone" data-label="Faites glisser vos documents ici" data-delete="contact_delete_document" data-save="contact_add_document" data-allowed="docx,pdf,txt,jpg,bmp,gif,xlsx,png,iso" class="form-control" id="document" name="document">
 				<?php echo json_encode($contact->documents()); ?>
 				<!-- Template action custom pour la dropzone -->
 				<div class="btn dropzone-custom-button hidden" onclick="alert('Je suis le fichier '+$(this).prev('a').text()+ '. Je vaux '+Math.floor((Math.random() * 10) + 1)+' bitcoins');" title="Une action custom" id="send-offer-button"><i class="fab fa-bitcoin"></i></div>
 			</div>
 			<br>
 			<label for="story">Histoire</label>
-			<small class="text-muted"> (Textarea data-type="wysiwyg")</small>
-			<textarea data-type="wysiwyg" class="form-control" id="story" name="story"><?php echo $contact->story; ?></textarea>
+			<small class="text-muted" data-placement="right" data-tooltip title="* data-label 			:	le label affiché dans la zone
+* data-mention-user='true' :activer les mentions d'utilisateurs interne
+* data-mention-object='action' :activer les mentions # sur une action
+* data-script-allow : désactive la supression automatique des tags <scripts> et <link> (anti xss)
+"> (Textarea data-type="wysiwyg")</small>
+			<textarea data-type="wysiwyg" class="" id="story" name="story" data-mention-user="true" data-mention-object='issue_autocomplete'><?php echo $contact->story; ?></textarea>
+			<label for="storyShort">Histoire dans un input</label>
+			<small class="text-muted"> (Textarea data-type="wysiwyg" et data-minimal)</small>
+			<textarea data-type="wysiwyg" class="form-control m-0" data-minimal id="storyShort" name="storyShort" data-mention-user="true" data-mention-object='issue_autocomplete'><?php echo $contact->storyShort; ?></textarea>
 			<br>
 			<label for="login">Login</label>
 			<small class="text-muted"> (Simple input texte)</small>
@@ -111,11 +140,11 @@ $contact = Contact::provide();
 			<br>
 			<label for="password">Password</label>
 			<small class="text-muted"> (Input data-type="password")</small>
-			<input type="text" data-type="password" data-toggle-event="hover" value="<?php echo $contact->password; ?>" placeholder="Mot de passe" class="form-control" id="password" name="password"/>
+			<input type="text" data-type="password" data-toggle-event="hover" data-generator value="<?php echo $contact->password; ?>" placeholder="Mot de passe" class="form-control" id="password" name="password"/>
 			<br>
 			<label for="contact">Nom du tuteur :</label>
 			<small class="text-muted"> (Div data-type="quickform")</small>
-			<div style="display: inline-block;" class="quickform" data-type="quickform" data-title="Ajout rapide de tuteur :" data-loaded="example_quickform_buttons" data-warning="Ajoutez rapidement un tuteur."  data-url="plugin/example/page.quick.example.php"><i class="fas fa-user-plus"></i></div>
+			<div class="quickform d-inline-block" data-type="quickform" data-title="Ajout rapide de tuteur :" data-loaded="example_quickform_buttons"  data-action="contact_quick_create"><i title="Ajoutez rapidement un tuteur." class="fas fa-user-plus"></i></div>
 			<input type="text" value="" placeholder="Nom du contact" class="form-control" id="contact" name="contact"/>
 			<br>
 			<label for="label">Donuts</label>
@@ -157,7 +186,7 @@ $contact = Contact::provide();
 			<small class="text-muted"> (div data-type="dictionnary-table")</small>
 			<div data-type="dictionnary-table" data-dictionnary="vehicles"></div>
 			<br><br>
-			<?php if($myFirm->has_plugin('fr.idleman.stripe')) : ?>
+			<?php if($myFirm->has_plugin('fr.sys1.stripe')) : ?>
 				<label for="checkbox">Formulaire de paiement en ligne (via API Stripe)</label>
 				<small class="text-muted"> (Div data-type="payment")</small><br>
 				<small class="text-muted">Il faut configurer les clés API en config avant de pouvoir réaliser un paiement. (Voir <a href="https://stripe.com/docs/testing#cards" target="_blank">cette page</a> pour des cartes bleues de tests)</small>
@@ -167,13 +196,15 @@ $contact = Contact::provide();
 		</div>
 		<div class="col-md-12 text-center noLabel">
 			<div class="btn btn-success" onclick="contact_save();"><i class="fas fa-check"></i> Enregistrer</div>
-       		<?php if($myUser->can('export', 'read') && $myFirm->has_plugin('fr.idleman.export')) : ?>
-			<div class="d-inline-block" data-type="export-model" data-callback="contact_export_callback" data-parameters='<?php echo stripslashes(json_encode(array("plugin"=>"example","dataset"=>"contact-sheet","id"=>$contact->id,"destination"=>addslashes('contact'.SLASH.'documents'.SLASH.$contact->id.SLASH)))); ?>'>
+       		
+       		<?php if($myUser->can('export', 'read') && $myFirm->has_plugin('fr.sys1.export')) : ?>
+			<div class="d-inline-block" data-type="export-model" data-pre-callback="contact_export_pre_callback" data-post-callback="contact_export_post_callback" data-parameters='<?php echo stripslashes(json_encode(array("plugin"=>"example","dataset"=>"contact-sheet","id"=>$contact->id,"destination"=>addslashes('contact'.SLASH.'documents'.SLASH.$contact->id.SLASH)))); ?>'>
 				<div class="btn btn-primary"><i class="far fa-file"></i> Export modèle</div>
 			</div>
        		<?php endif; ?>
 
+			<div class="btn btn-info" onclick='sendmail_preview(<?php echo json_encode($data); ?>);'><i class="fas fa-paper-plane"></i> Envoi mail</div>
 			<br><br>
 		</div>
 	</form>
-</div>
+</div>

+ 5 - 6
plugin/example/setting.example.php

@@ -4,11 +4,10 @@ User::check_access('example','configure');
 ?>
 <div class="row">
 	<div class="col-md-12">
-		
-
-		<div id="example-doc">Chargement en cours</div>
-
-
-
+		<br>
+		<div class="btn btn-success float-right" onclick="example_setting_save()"><i class="fas fa-check"></i> Enregistrer</div>
+		<h3>Réglages Example</h3>
+		<hr/>
+		<?php echo Configuration::html('example'); ?>
 	</div>
 </div>

+ 97 - 82
plugin/export/ExportModel.class.php

@@ -34,13 +34,15 @@ class ExportModel extends Entity{
 		foreach(glob(__ROOT__.FILE_PATH.'export'.SLASH.'documents'.SLASH.$this->plugin.SLASH.$this->id.SLASH.'*.*') as $file){
 			if(is_dir($file)) continue;
 			$filenameDisk = $file;
-			// $filenameDisk = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? html_decode_utf8($file) : $file;
-			if(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') $file = utf8_encode($file);
+			// $filenameDisk = get_OS() === 'WIN' ? html_decode_utf8($file) : $file;
+			if(get_OS() === 'WIN') $file = utf8_encode($file);
+			$ext = getExt($file);
 			$documents[] = array(
 				'path' => 'export'.SLASH.'documents'.SLASH.$this->plugin.SLASH.$this->id.SLASH.basename($file),
-				'url' => 'action.php?action=export_exportmodel_download_document&path='.$this->plugin.SLASH.$this->id.SLASH.rawurlencode(basename($file)),
+				'url' => 'action.php?action=export_model_download_document&path='.$this->plugin.SLASH.$this->id.SLASH.rawurlencode(basename($file)),
 				'name' => basename($file),
-				'icon' => getExtIcon(getExt($file)),
+				'ext' => $ext,
+				'icon' => getExtIcon($ext),
 				'lastModification' => ' - '.date('d/m/Y', filemtime($filenameDisk))
 			);
 		}
@@ -49,57 +51,60 @@ class ExportModel extends Entity{
 
 	public static function get_standard_dataset($parameters){
 		global $myUser, $myFirm;
-		if(isset($parameters['description']) && $parameters['description']!=true){
-			//Common DS
-			$data['programme.date.compact'] = date('d/m/Y');
-			setlocale (LC_TIME, 'fr_FR.utf8','fra');
-			$data['programme.date.littéral'] = utf8_encode(strftime('%d %B %Y'));
-			$data['programme.heure'] = date('H:i');
-			$data['utilisateur.photo'] = '::'.$myUser->getAvatar(true);
-			$data['utilisateur.identifiant'] = $myUser->login;
-			$data['utilisateur.prénom'] = $myUser->firstname;
-			$data['utilisateur.nom'] = $myUser->name;
-			$data['utilisateur.fonction'] = $myUser->function;
-			$data['utilisateur.email'] = $myUser->mail;
-			
-			$data['établissement.logo'] = '::'.$myFirm->logo('path');
-			$data['établissement.libellé'] = $myFirm->label;
-			$data['établissement.téléphone'] = $myFirm->phone;
-			$data['établissement.fax'] = $myFirm->fax;
-			$data['établissement.adresse.rue'] = $myFirm->street;
-			$data['établissement.adresse.complément'] = $myFirm->street2;
-			$data['établissement.adresse.ville'] = $myFirm->city;
-			$data['établissement.adresse.cp'] = $myFirm->zipcode;
-			$data['établissement.email'] = $myFirm->mail;
-			$data['établissement.siret'] = $myFirm->siret;
-		} else {
-			//Common DS
-			$data['programme.date.compact'] = 'Date courante (format "dd/mm/YYYY")';
-			$data['programme.date.littéral'] = 'Date courante (format "01 Janvier 1970")';
-			$data['programme.heure'] = 'Heure courante (format hh:mm)';
-			$data['utilisateur.photo'] = array('desc'=>'Photo de profil de l\'utilisateur courant', 'type'=>'photo');
-			$data['utilisateur.identifiant'] = 'Identifiant de l\'utilisateur courant';
-			$data['utilisateur.prénom'] = 'Prénom de l\'utilisateur courant';
-			$data['utilisateur.nom'] = 'Nom de l\'utilisateur courant';
-			$data['utilisateur.fonction'] = 'Fonction occupée par l\'utilisateur courant';
-			$data['utilisateur.email'] = 'Adresse e-mail de l\'utilisateur courant';
-
-			$data['établissement.logo'] = array('desc'=>'Le logo de l\'établissement courant', 'type'=>'photo');
-			$data['établissement.libellé'] = 'Le libellé de l\'établissement courant';
-			$data['établissement.téléphone'] = 'Le n° de téléphone de l\'établissement courant';
-			$data['établissement.fax'] = 'Le n° de fax de l\'établissement courant';
-			$data['établissement.adresse.rue'] = 'La rue de l\'établissement courant';
-			$data['établissement.adresse.complément'] ='Le complément d\'adresse de l\'établissement courant';
-			$data['établissement.adresse.ville'] ='La ville de l\'établissement courant';
-			$data['établissement.adresse.cp'] = 'Le code postal de l\'établissement courant';
-			$data['établissement.email'] = 'L\'adresse mail de l\'établissement courant';
-			$data['établissement.siret'] = 'Le n° de SIRET de l\'établissement courant';
-		}
+		setlocale (LC_TIME, 'fr_FR.utf8','fra');
+		
+		//Common DS
+		$data['programme']  = array('type'=>'object','value'=>array(
+			'date' => array(
+				'label'=>'Date courante',
+				'type'=>'object',
+				'value' => array(
+					'compact' => array('label'=>'Date courante (format "dd/mm/YYYY")','value'=>date('d/m/Y')),
+					'littéral' => array('label'=>'Date courante (format "01 Janvier 1970")','value'=>strftime('%d %B %Y'))
+				)
+			),
+			'heure' => array('label'=>'Heure courante (format hh:mm)','value'=> date('H:i'))
+		));
+
+		$data['utilisateur'] = array('type'=>'object','value'=>array(
+			'identifiant' => array('label'=>'Identifiant de l\'utilisateur courant','value'=>$myUser->login),
+			'prénom' => array('label'=>'Prénom de l\'utilisateur courant','value'=>$myUser->firstname),
+			'nom' => array('label'=>'Nom de l\'utilisateur courant','value'=>$myUser->name),
+			'fonction' => array('label'=>'Fonction occupée par l\'utilisateur courant','value'=>$myUser->function),
+			'email' => array('label'=>'Adresse e-mail de l\'utilisateur courant','value'=>$myUser->mail),
+			'photo' => array(
+				'label'=>'Photo de profil de l\'utilisateur courant', 
+				'type'=>'image',
+				'value' => file_get_contents($myUser->getAvatar(true))
+			)
+		));
+
+		$data['établissement'] = array('type'=>'object','value'=>array(
+			'logo' => array(
+				'label'=>'Le logo de l\'établissement courant', 
+				'type'=>'image',
+				'value' => file_get_contents($myFirm->logo('path')),
+			),
+			'libellé' => array('label'=>'Le libellé de l\'établissement courant','value'=>$myFirm->label),
+			'téléphone' => array('label'=>'Le n° de téléphone de l\'établissement courant','value'=>$myFirm->phone),
+			'fax' => array('label'=>'Le n° de fax de l\'établissement courant','value'=>$myFirm->fax),
+			'email' => array('label'=>'L\'adresse mail de l\'établissement courant','value'=>$myFirm->mail),
+			'siret' => array('label'=>'Le n° de SIRET de l\'établissement courant','value'=>$myFirm->siret),
+		));
+
+		$data['établissement']['value']['adresse'] = array('type'=>'object','value'=>array(
+			'rue' => array('label'=>'La rue de l\'établissement courant','value'=>$myFirm->street),
+			'complément' => array('label'=>'Le complément d\'adresse de l\'établissement courant','value'=>$myFirm->street2),
+			'ville' => array('label'=>'La ville de l\'établissement courant','value'=>$myFirm->city),
+			'cp' =>  array('label'=>'Le code postal de l\'établissement courant','value'=>$myFirm->zipcode),
+		));
+
 		return $data;
 	}
 
 	//Renvoie les différents type de templates pris en compte
 	public static function templates($key=null){
+
 		$templates = array();
 		foreach (glob(__DIR__.SLASH.'template'.SLASH.'*.class.php') as $templatePath) {
 			require_once($templatePath);
@@ -129,7 +134,6 @@ class ExportModel extends Entity{
 
 	//Récuperation de toutes les données (ou description) d'un dataset
 	public static function dataset($plugin,$dataset,$parameters=array(),$description = null){
-		
 		$datasets = array();
 	
 		Plugin::callHook('export_model_data', array(
@@ -140,48 +144,59 @@ class ExportModel extends Entity{
 		)));
 
 		$current = reset($datasets);
+		$data = $current['function']($parameters);
 		$current['values'] = array();
 		//Merge des données génériques erp et des données du dataset
-		$allDataset = array_merge(
-			ExportModel::get_standard_dataset($parameters), 
-			$current['function']($parameters)
+		$allDataset = array_merge_recursive(
+			ExportModel::get_standard_dataset($parameters),
+			(!$data?array():$data)
 		);
+		
+		$current['values'] = self::recursive_dataset_empty($allDataset);
+		return $current;
+	}
+
+	public static function rawData($dataset){
+		foreach ($dataset as $key => $value) {
 
-		foreach ($allDataset as $macro => $value) {
-			$row = array();
-			if(is_array($value)){
-				$row['type'] = $value['type'];
-				$row['desc'] = $value['desc'];
-				if(isset($value['subitems']))
-					$row['subitems'] = $value['subitems'];
-			} else {
-				$row['type'] = 'value';
-				$row['desc'] = $value;
+			if(is_array($value) && array_key_exists('value', $value)){
+			 	if(is_array($value['value'])) $value['value'] = self::rawData($value['value']);
+				$dataset[$key] = empty($value['value']) ? '' : $value['value'];
+			}else{
+				if(is_array($value)) $dataset[$key] = self::rawData($value);
 			}
-			$current['values'][$macro] = $row;
 		}
-		return $current;
+		return $dataset;
 	}
 
-	//Permet de fusionner le fichier d'export modèle et les données associées
-	public static function merge_data_template(&$filename, $docInfos, $datas, $return = 'stream'){
-		$document = $docInfos['document'];
-		$ext = getExt($document['name']);
-		$tplType = ExportModel::templates($docInfos['format']);
-		$tplHandler = $tplType['handler'];
-
-		//Gestion macros de nombre et macro dans le titre
-		foreach($datas as $key => $value){
-			if(is_array($value)){
-				$datas[$key.'.nombre'] = count($value);
-				continue;
+
+	public static function recursive_dataset_empty($set){
+		$format = array();
+		foreach ($set as $macro => $row) {
+
+			if(is_array($row)){
+				if(!empty($row['value']) &&  empty($row['type'])) $row['type'] = 'value';
+				$row = self::recursive_dataset_empty($row);
 			}
-			$filename = str_replace('{{'.$key.'}}', $value, $filename);
+			if(isset($row['value']) && is_string($row['value'])){
+				$row['value'] = '';
+			}
+		
+			$format[$macro] = $row;
 		}
-		$filename = preg_replace('#[\<\>\?\|\*\:\\\/\"\'\,]#i', '-', $filename);
-		$filePath = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($document['path']) : $document['path'];
+		return $format;
+	}
 
-		return $tplHandler::from_template($filePath, $datas, $return);
+	//Export d'un set de donnée $datas dans le template $modelStream en fonction du type de document $type
+	// ex : ExportModel::export('CsvExport', 'col1;{{name}}',array('name'=> array('value' => 'toto') ));
+	public static function export($type, $modelStream, $datas, $parameters=array()){
+		require_once(__DIR__.SLASH.'template'.SLASH.$type.'.class.php');
+		$instance = new $type();
+		
+		if(method_exists($instance,'start')) $instance->start($modelStream,$datas, $parameters);
+		$stream = $instance->from_template($modelStream, $datas);
+		if(method_exists($instance,'end')) $stream = $instance->end($stream, $datas, $parameters);
+		return $stream;
 	}
 }
 ?>

+ 109 - 105
plugin/export/action.php

@@ -4,7 +4,7 @@ switch($_['action']){
 	/** TEMPLATE **/
 	
 	//Récuperation d'une liste de exportmodel
-	case 'export_exportmodel_search':
+	case 'export_model_search':
 		Action::write(function(&$response){
 			global $myUser,$_;
 			User::check_access('export','read');
@@ -15,8 +15,8 @@ switch($_['action']){
 				//du plugin et jeu de données depuis le composant
 				//d'export modèle
 				$params = $_['params'];
-				if(not_set_empty($params['plugin'])) throw new Exception("Il faut préciser le plugin ciblé");
-				if(not_set_empty($params['dataset'])) throw new Exception("Il faut préciser le jeu de données à récupérer");
+				if(!isset($params['plugin']) || empty($params['plugin'])) throw new Exception("Il faut préciser le plugin ciblé");
+				if(!isset($params['dataset']) || empty($params['dataset'])) throw new Exception("Il faut préciser le jeu de données à récupérer");
 
 				$query = 'SELECT * FROM '.ExportModel::tableName().' WHERE plugin = ? AND dataset = ? AND (privacy = "'.ExportModel::PRIVACY_PUBLIC.'" OR (privacy = "'.ExportModel::PRIVACY_PRIVATE.'" AND creator = "'.$myUser->login.'"))';
 				$data = array($params['plugin'], $params['dataset']);
@@ -28,7 +28,7 @@ switch($_['action']){
 				foreach(ExportModel::staticQuery('SELECT * FROM '.ExportModel::tableName().' WHERE dataset IS NULL', array(), true) as $exMdl) {
 					ExportModel::deleteById($exMdl->id);
 					foreach ($exMdl->documents() as $doc) {
-						$path = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($doc['path']) : $doc['path'];
+						$path = (get_OS() === 'WIN') ? utf8_decode($doc['path']) : $doc['path'];
 						unlink(File::dir().$path);
 					}
 				}
@@ -69,7 +69,7 @@ switch($_['action']){
 					$query .= ' ORDER BY plugin ASC, created DESC';
 
 				//Pagination
-				$response['pagination'] = ExportModel::paginate(10,(!empty($_['page'])?$_['page']:0),$query,$data);
+				$response['pagination'] = ExportModel::paginate(20,(!empty($_['page'])?$_['page']:0),$query,$data);
 				
 				//Mise en forme des résultats
 				foreach(ExportModel::staticQuery($query,$data,true) as $exportmodel){
@@ -78,36 +78,56 @@ switch($_['action']){
 					$row['datasetName'] = $datasets[$exportmodel->dataset]['label'];
 					$row['class'] = $exportmodel->privacy==ExportModel::PRIVACY_PRIVATE ? 'private' : 'public';
 					$row['privacy'] = $exportmodel->privacy();
+					$row['icon'] = !empty($exportmodel->export_format) ? getExtIcon($exportmodel->export_format) : getExtIcon(getExt($exportmodel->filename));
 					$response['rows'][] = $row;
 				}
 			}
 		});
 	break;
 	
+	case 'export_model_format_refresh':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('export','edit');
+			require_once(__DIR__.SLASH.'ExportModel.class.php');
+			$response['rows'] = array();
+			
+			switch($_['ext']){
+				case 'html':
+					$response['rows'][] = array('uid' => 'html', 'label' => 'HTML');
+					$response['rows'][] = array('uid' => 'pdf', 'label' => 'PDF');
+				break;
+				default:
+				break;
+			}
+		});
+	break;
+
 	//Ajout ou modification d'élément exportmodel
-	case 'export_exportmodel_save':
+	case 'export_model_save':
 		Action::write(function(&$response){
 			global $myUser,$_;
 			User::check_access('export','edit');
 			require_once(__DIR__.SLASH.'ExportModel.class.php');
 
-			if(not_set_empty($_['label'])) throw new Exception('Champ "Libellé" obligatoire');
-			// if(not_set_empty($_['description'])) throw new Exception('Champ "Description" obligatoire');
-			if(not_set_empty($_['plugin'])) throw new Exception('Champ "Plugin" obligatoire');
-			if(not_set_empty($_['dataset'])) throw new Exception('Champ "Jeu de données" obligatoire');
+			if(!isset($_['label']) || empty($_['label'])) throw new Exception('Champ "Libellé" obligatoire');
+			// if(!isset($_['description']) || empty($_['description']))) throw new Exception('Champ "Description" obligatoire');
+			if(!isset($_['plugin']) || empty($_['plugin'])) throw new Exception('Champ "Plugin" obligatoire');
+			if(!isset($_['dataset']) || empty($_['dataset'])) throw new Exception('Champ "Jeu de données" obligatoire');
 
 			$item = ExportModel::provide();
-			if(isset($_['slug']) && !empty($_['slug']) && ExportModel::load(array('slug'=>$_['slug'], 'id:!='=>$item->id))) throw new Exception("Le slug (".slugify($_['slug']).") est déjà utilisé pour un autre export modèle");
+			$_['slug'] = !empty($_['slug']) ? slugify($_['slug']) : (!empty($item->slug) ? $item->slug : slugify($_['label']));
+			if(!empty($_['slug']) && ExportModel::load(array('slug'=>$_['slug'], 'id:!='=>$item->id))) throw new Exception("Le slug (".$_['slug'].") est déjà utilisé pour un autre export modèle");
 			$newItem = isset($item->id) && !empty($item->id) ? false : true;
 			$docs = $item->documents();
 			//Ajout des fichiers joints
-			if(not_set_empty($_['document_temporary']) && empty($docs)) throw new Exception("Un fichier de modèle est requis");
+			if((!isset($_['document_temporary']) || empty($_['document_temporary'])) && empty($docs)) throw new Exception("Un fichier de modèle est requis");
 
 			$item->label = $_['label'];
 			$item->description = $_['description'];
 			$item->plugin = $_['plugin'];
 			$item->dataset = $_['dataset'];
-			$item->slug = slugify($_['slug']);
+			$item->slug = $_['slug'];
 			$item->privacy = $_['privacy']==1 ? ExportModel::PRIVACY_PRIVATE : ExportModel::PRIVACY_PUBLIC;
 			$item->save();
 
@@ -125,11 +145,11 @@ switch($_['action']){
 
 				foreach($files as $file){
 					$item->filename = $file['name'];
-					$from = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? File::temp().utf8_decode($file['path']) : File::temp().$file['path'];
-					$to = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($file['name']) : $file['name'];
+					$from = (get_OS() === 'WIN') ? File::temp().utf8_decode($file['path']) : File::temp().$file['path'];
+					$to = (get_OS() === 'WIN') ? utf8_decode($file['name']) : $file['name'];
 					$fileReturn = File::move($from, 'export'.SLASH.'documents'.SLASH.$item->plugin.SLASH.$item->id.SLASH.$to);
 					$response['relativePath'] = $fileReturn['relative'];
-					$response['filePath'] = 'action.php?action=export_exportmodel_download_document&path='.rawurlencode($item->plugin.SLASH.$item->id.SLASH.$to);
+					$response['filePath'] = 'action.php?action=export_model_download_document&path='.rawurlencode($item->plugin.SLASH.$item->id.SLASH.$to);
 				}
 			}
 			if(count($docs)==1) {
@@ -143,12 +163,12 @@ switch($_['action']){
 			$item->save();
 			$response['id'] = $item->id;
 
-			Log::put("Création/Mofdification d'export modèle ".$item->toText(), "Export Modèle");
+			Log::put("Création/Modification d'export modèle ".$item->toText(), "Export Modèle");
 		});
 	break;
 	
 	//Récuperation ou edition d'élément exportmodel
-	case 'export_exportmodel_edit':
+	case 'export_model_edit':
 		Action::write(function(&$response){
 			global $myUser,$_;
 			User::check_access('export','edit');
@@ -158,7 +178,7 @@ switch($_['action']){
 	break;
 
 	//Suppression d'élement exportmodel
-	case 'export_exportmodel_delete':
+	case 'export_model_delete':
 		Action::write(function(&$response){
 			global $myUser,$_;
 			User::check_access('export','delete');
@@ -169,7 +189,7 @@ switch($_['action']){
 
 			$docsProps = $item->documents();
 			foreach ($docsProps as $doc) {
-				$path = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($doc['path']) : $doc['path'];
+				$path = (get_OS() === 'WIN') ? utf8_decode($doc['path']) : $doc['path'];
 				unlink(File::dir().$path);
 			}
 
@@ -177,13 +197,12 @@ switch($_['action']){
 			// $item = ExportModel::getById($_['id']);
 			// $item->state = ExportModel::INACTIVE;
 			// $item->save();
-			
 			Log::put("Suppression d'export modèle ".$item->toText(), "Export Modèle");
 		});
 	break;
 
 	//Ajout document automatique à l'upload
-	case 'export_exportmodel_add_document':
+	case 'export_model_add_document':
 		Action::write(function(&$response){
 			global $myUser,$_;
 			User::check_access('export','edit');
@@ -208,11 +227,12 @@ switch($_['action']){
 				$ext = getExt($file['name']);
 				$exportmodel->export_format = $ext;
 
-				$name = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($file['name']) : $file['name'];
+				$name = (get_OS() === 'WIN') ? utf8_decode($file['name']) : $file['name'];
 				$exportmodel->filename = $file['name'];
 				$row = File::move(File::temp().$file['path'],'export'.SLASH.'documents'.SLASH.$exportmodel->plugin.SLASH.$exportmodel->id.SLASH.$name);
-				$row['url'] = 'action.php?action=export_exportmodel_download_document&path='.SLASH.$exportmodel->plugin.SLASH.$exportmodel->id.SLASH.rawurlencode($file['name']);
+				$row['url'] = 'action.php?action=export_model_download_document&path='.SLASH.$exportmodel->plugin.SLASH.$exportmodel->id.SLASH.rawurlencode($file['name']);
 				$row['oldPath'] = $file['path'];
+				$row['ext'] = $file['ext'];
 				$response['files'][] = $row;
 			}
 			$exportmodel->save();
@@ -223,13 +243,13 @@ switch($_['action']){
 	break;
 
 	//Suppression document
-	case 'export_exportmodel_delete_document':
+	case 'export_model_delete_document':
 		Action::write(function(&$response){
 			global $myUser,$_;
 			User::check_access('export','delete');
 			require_once(__DIR__.SLASH.'ExportModel.class.php');
 			if(!isset($_['path']) ) throw new Exception("Chemin non spécifié ou non numerique");
-			$path = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($_['path']) : $_['path'];
+			$path = (get_OS() === 'WIN') ? utf8_decode($_['path']) : $_['path'];
 			//Le premier argument est un namespace de sécurité 
 			//et assure que le fichier sera toujours cloisoné dans un contexte file/export/documents
 			File::delete('export'.SLASH.'documents',$path);
@@ -243,17 +263,17 @@ switch($_['action']){
 	break;
 
 	//Téléchargement des documents
-	case 'export_exportmodel_download_document':
+	case 'export_model_download_document':
 		global $myUser,$_;
 		User::check_access('export','read');
-		$path = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($_['path']) : $_['path'];
+		$path = (get_OS() === 'WIN') ? utf8_decode($_['path']) : $_['path'];
 		File::downloadFile(File::dir().'export'.SLASH.'documents'.SLASH.$path);
 
 		Log::put("Téléchargement du fichier modèle : ".$path, "Export Modèle");
 	break;
 
 	//Téléchargement des templates d'exemple
-	case 'export_exportmodel_download_template':
+	case 'export_model_download_template':
 		global $myUser,$_;
 		User::check_access('export','read');
 		if(!isset($_['extension']) || empty($_['extension'])) throw new Exception("Extension non précisée");
@@ -261,31 +281,17 @@ switch($_['action']){
 		$template = ExportModel::templates($_['extension']);
 		$dataset = ExportModel::dataset($_['plugin'],$_['dataset']);
 		$className =  $template['handler'];
-		$stream = $className::sample($dataset['values']);
+		$instance = new $className();
 
+		$stream = $instance->sample($dataset['values']);
+		
 		File::downloadStream($stream, $_['filename'].'.'.$template['extension'],$template['mime']);
 
 		Log::put("Téléchargement d'un fichier modèle d'exemple : ".$_['filename'].'.'.$template['extension'], "Export Modèle");
 	break;
 
-	//Récupération des jeux de données en fct de params
-	case 'export_exportmodel_list_dataset':
-		Action::write(function(&$response){
-			global $myUser,$_;
-			User::check_access('export','read');
-			require_once(__DIR__.SLASH.'ExportModel.class.php');
-	
-			$params = isset($_['params']) ? $_['params'] : array();
-			$datasets = array();
-			Plugin::callHook('export_model_data', array(&$datasets, $params));
-		
-			$response['rows'] = $datasets;
-		});
-	break;
-
-
 	//Récupération du détail d'un jeu de donnée
-	case 'export_exportmodel_get_dataset':
+	case 'export_model_get_dataset':
 		Action::write(function(&$response){
 			global $myUser,$_;
 			User::check_access('export','read');
@@ -299,90 +305,88 @@ switch($_['action']){
 			$params['description'] = true;
 			$set = ExportModel::dataset($plugin, $dataset, $params);
 			$response['dataset'] = $set['values'];
-
+			
 			foreach(ExportModel::templates() as $extension => $template){
 				$response['files'][] = array(
-					'path'=>'action.php?action=export_exportmodel_download_template&plugin='.rawurlencode($plugin).'&dataset='.rawurlencode($dataset).'&extension='.$template['extension'].'&filename='.$set['label'],
+					'path'=>'action.php?action=export_model_download_template&plugin='.rawurlencode($plugin).'&dataset='.rawurlencode($dataset).'&extension='.$template['extension'].'&filename='.$set['label'],
 					'icon'=>getExtIcon($template['extension']),
 					'ext'=>$template['extension'],
 					'label'=> $set['label'].'.'.$template['extension']
 				);
 			}
-
 		});
 	break;
 
-
 	//Choix du exportmodel pour export modèle
-	case 'export_exportmodel_export':
-		try{
-			ob_start();
+	case 'export_model_export':
+		Action::write(function(&$response){
 			global $myUser,$_;
 			User::check_access('export','read');
 			require_once(__DIR__.SLASH.'ExportModel.class.php');
+				
+			$parameters = isset($_['parameters']) ? $_['parameters'] : array();
+			if(!isset($parameters['model']) || empty($parameters['model'])) throw new Exception("Il faut choisir un modèle d'export");
 
-			if(!isset($_['exportmodel']) || empty($_['exportmodel'])) throw new Exception("Il faut choisir un modèle d'export");
-
-			$params = isset($_['params']) ? json_decode(base64_decode($_['params']), true) : array();
-			$params['description'] = false;
-			$return = isset($params['destination']) && !empty($params['destination']) ? $params['destination'] : 'stream';
+			$parameters['description'] = false;
+		
+			//Récuperation du fichier modèle sélectionné
+			$model = ExportModel::getById($parameters['model']);
+			if(!$model) throw new Exception("Impossible de récupérer le modèle d'export");
 
-			if($exportmodel = ExportModel::getById(base64_decode($_['exportmodel']))){
-				$plugin = $exportmodel->plugin;
-				$dataset = $exportmodel->dataset;
-			} else {
-				throw new Exception("Impossible de récupérer le modèle d'export");
-			}
-			//Tableau de jeu de données
+			//Récuperation des données brutes tout dataset confondus
 			$datasets = array();
-
-			//On alimente notre tableau de jeu de données en 
-			//appelant un hook qui sera défini pour remplir le tableau
-			Plugin::callHook('export_model_data', array(&$datasets, $params));
-	
-			//On a notre tableau de jeu de données rempli et on 
-			//récupère la partie de notre jeu de donnée qui nous 
-			//intéresse et on execute la fonction associée
-			if(!isset($datasets[$dataset])) throw new Exception("Jeu de données spécifié inexistant");
-			$currentDataset = $datasets[$dataset];
-			$plgLines = $currentDataset['function']($params);
-			$stdLines = ExportModel::get_standard_dataset($params);
-			$lines = array_merge($stdLines, $plgLines);
+			Plugin::callHook('export_model_data', array(&$datasets, $parameters));
+			
+			//On  récupère uniquement le dataset qui nous intéresse et on execute la fonction associée
+			if(!isset($datasets[$model->dataset])) throw new Exception("Jeu de données spécifié inexistant");
+			$dataset = $datasets[$model->dataset];
+			
+			//On ajoute les données standard (comptes connecté etc..) aux set de donnée à associer au template
+			$dataset = array_merge_recursive(ExportModel::get_standard_dataset($parameters), $dataset['function']($parameters));
 	
-			//On génère le fichier à exporter en utilisant le jeu de données 
+			//On génère le fichier à exporter en utilisant le jeu de données ($datas)
 			//+ le fichier associé à l'exportmodel (le fichier mis en dropzone)
-			$docsProps = $exportmodel->documents();
-			if(empty($docsProps)) throw new Exception("Aucun fichier modèle pour le modèle d'export : ".$exportmodel->label);
+			$templates = $model->documents();
+			if(empty($templates)) throw new Exception("Aucun fichier modèle pour le modèle d'export : ".$model->label);
 
-			foreach ($docsProps as $document) {
-				$docInfos['document'] = $document;
-				$docInfos['format'] = $exportmodel->export_format;
+			foreach ($templates as $document) {
 				$ext = getExt($document['name']);
-				$tplType = ExportModel::templates($docInfos['format']);
-				$filename = basename($document['name'], $ext).$docInfos['format'];
+				//Type d'export (csv, excel, html, word ...)
+				$type = ExportModel::templates($model->export_format);
+				$filename = basename($document['name'], $ext).$model->export_format;
 
-				$fileReturn = ExportModel::merge_data_template($filename, $docInfos, $lines,$return);
+				$filePath = (get_OS() === 'WIN') ? utf8_decode($document['path']) : $document['path'];
+				
+				//Les types d'export prennent toujours de l'utf8 en entrée
+				$stream = file_get_contents(File::dir().$filePath);
+				if(mb_detect_encoding($stream, 'UTF-8', true) == false) $stream = utf8_encode($stream);
+
+				$datas = ExportModel::rawData($dataset);
 
-				if($return == 'stream') {
-					File::downloadStream($fileReturn, $filename, $tplType['mime']);
+				//Gestion macros dans le titre
+				$filename = ExportModel::export('TextExport', $filename, $datas, $parameters);
+				
+				//On exporte le jeux de donnée pour le flux modèle séelctionné
+				$stream = ExportModel::export($type['handler'], $stream, $datas, $parameters);
+
+				Log::put("Exportation de modèle, plugin : ".$model->plugin.", jeu de données : ".$model->dataset.".", "Export Modèle");
+				if(empty($parameters['destination']) || $parameters['destination'] == 'stream') {
+					$response['filename'] = $filename;
+					$response['type'] = $type['mime'];
+					File::downloadStream($stream, $filename, $type['mime']);
+					exit();
 				} else {
-					$filename = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? utf8_decode($filename) : $filename;
-					$filename = autoincrement_filename($docInfos['format'], $return, $filename);
-					$fileInfo = File::move($fileReturn, $return.$filename);
-					$previousUrl = base64_decode($_['url']);
-					header('location: '.$previousUrl.'&success=Export modèle correctement généré');
+					$filename = preg_replace('#[\<\>\?\|\*\:\\\/\"\'\,]#i', '-', $filename);
+					$filename = (get_OS() === 'WIN') ? utf8_decode($filename) : $filename;
+					$filename = autoincrement_filename($model->export_format, $parameters['destination'], $filename);
+					$response['filename'] = (get_OS() === 'WIN') ? utf8_encode($filename) : $filename;
+
+					$finalPath = File::dir().$parameters['destination'];
+					if(!file_exists($finalPath)) mkdir($finalPath,0755,true);
+					$fileInfo = file_put_contents($finalPath.$filename, $stream);
 				}
 			}
-			Log::put("Exportation de modèle, plugin : ".$plugin.", jeu de données : ".$dataset.".", "Export Modèle");
-		} catch(Exception $e){
-			$previousUrl = base64_decode($_['url']);
-			header('location: '.$previousUrl.'&error='.$e->getMessage());
-			exit();
-		}
+		});
 	break;
 }
-
-
-
-
 ?>

+ 2 - 2
plugin/export/app.json

@@ -1,11 +1,11 @@
 {
-	"id": "fr.idleman.export",
+	"id": "fr.sys1.export",
 	"name": "Export",
 	"author" : {
 		"name" : "Valentin MORREEL"
 	},
 	"version": "1.0",
-	"url": "http://idleman.fr",
+	"url": "http://sys1.fr",
 	"licence": {"name": "Copyright","url" : ""},
 	"description": "Plugin d'export de données via des modèles (.docx, .xlsx, .txt, etc..)",
 	"require" : {}

+ 16 - 12
plugin/export/css/main.css

@@ -1,6 +1,6 @@
 /** EXPORT **/
 .table-exportmodels .exportmodel-public {
-	color: #1e998d;
+	color: #007bff;
 }
 .table-exportmodels .exportmodel-public:before {
     font-family: 'Font Awesome 5 Free';
@@ -9,7 +9,7 @@
     margin-right: 5px;
 }
 .table-exportmodels .exportmodel-private {
-	color: #ca023e;
+	color: #dc3545;
 }
 .table-exportmodels .exportmodel-private:before {
     font-family: 'Font Awesome 5 Free';
@@ -51,36 +51,40 @@
 	z-index: 1030;
 	position: absolute;
 }
-.dataset-container > div.row > div {
-	padding: 15px;
-}
 .dataset-showcase > ul {
 	list-style: none;
 	padding-left: 0;
 }
 .exportmodel-form .badge {
 	display: inline-block;
-	width: 45px;
 	margin-right: 5px;
 }
+.export-model-documentation .badge.macro-list,
 .exportmodel-form .badge.macro-list {
 	color: #212529;
 	background-color: #ffc107;
 }
+.export-model-documentation .badge.macro-object,
+.exportmodel-form .badge.macro-object {
+	color: #ffffff;
+	background-color: #28a745;
+}
+.export-model-documentation .badge.macro-value,
 .exportmodel-form .badge.macro-value {
 	color: #ffffff;
 	background-color: #007bff;
 }
-.exportmodel-form .badge.macro-photo {
+.export-model-documentation .badge.macro-image,
+.exportmodel-form .badge.macro-image {
 	color: #ffffff;
     background-color: #dc3545;
 }
 .dataset-showcase ul li {
     cursor: pointer;
-    padding: 0 5px;
     padding-bottom: 3px;
+    transition: all 0.05s ease-in;
 }
-.dataset-showcase ul li:not(.li-list):hover {
+.dataset-showcase ul li:hover {
     background: #e9e9e99c;
     border-radius: 1rem;
 }
@@ -100,14 +104,14 @@
 .dataset-example-files li i {
 	margin: 0 5px;
 }
-.export-doc .doc-picture {
+.export-model-documentation .doc-picture {
 	width: 100px;
 	height: 100px;
 }
-.export-doc > div {
+.export-model-documentation > div {
 	margin-bottom: 25px;
 }
-.export-doc li {
+.export-model-documentation li {
 	margin-bottom: 5px;
 }
 

+ 2 - 2
plugin/export/export.plugin.php

@@ -13,13 +13,13 @@ function export_page(){
 
 //Fonction executée lors de l'activation du plugin
 function export_install($id){
-	if($id != 'fr.idleman.export') return;
+	if($id != 'fr.sys1.export') return;
 	Entity::install(__DIR__);
 }
 
 //Fonction executée lors de la désactivation du plugin
 function export_uninstall($id){
-	if($id != 'fr.idleman.export') return;
+	if($id != 'fr.sys1.export') return;
 	if(is_dir(File::dir().'export')) delete_folder_tree(File::dir().'export');
 	Entity::uninstall(__DIR__);
 }

+ 33 - 10
plugin/export/js/component.js

@@ -1,17 +1,32 @@
-//Initialisation du composant d'export modèle
+/**
+ * Initialisation du composant export modèle
+ *
+ * data-default 	: slug du modèle d'export à mettre par défaut
+ * data-parameters  : les paramètres à renseigner pour l'export modèle
+ * 		=> doit contenir les clés suivantes :
+ * 			- plugin  (obligatoire) : le plugin ciblé
+ * 			- dataset (obligatoire) : le jeu de données ciblé
+ * 			- destination (facultatif) : chemin où enregistrer le fichier, ne pas mettre si on veut télécharger le fichier généré à la volée
+ * 			- autres paramètres personnalisés...
+ * data-pre-callback  : string de la fonction appelée avant export (juste après affichage de modal)
+ * data-post-callback : string de la fonction appelée après export
+ * 
+ * @param  jQuery object input l'input qui servira de support pour le composant
+ * @return rien       Affiche le modal avec les modèles trouvés pour le plugin et dataset défini
+ */
 function init_components_export_model(input){
-	var cbLoaded = input.attr('data-callback') ? input.attr('data-callback') : '';
-	var cbParams = input.attr('data-callback-parameters') ? input.attr('data-callback-parameters').split(',') : [];
+	var preCallback = input.attr('data-pre-callback') ? input.attr('data-pre-callback') : null;
+	var postCallback = input.attr('data-post-callback') ? input.attr('data-post-callback') : null;
 	var parameters = JSON.parse(input.attr('data-parameters'));
 
 	$(document).ready(function(e){
-		input.on('click', function(e){
+		input.off('click').on('click', function(e){
 			$.ajax({
 				type: 'GET',
 				url: 'plugin/export/modal.export.model.php',
 				async: true,
 				success : function(modal){
-					if(cbLoaded) window[cbLoaded].apply(null,cbParams);
+					if(preCallback!=null) window[preCallback]();
 				}
 			}).done(function(modalContent){
 				if(!$('#export-modal').length)
@@ -20,26 +35,34 @@ function init_components_export_model(input){
 				reset_inputs(modal);
 
 				$.action({
-					action: 'export_exportmodel_search',
+					action: 'export_model_search',
 					params: parameters
 				}, function(r){
+					var selectExport = $('#exportModel');
 					if(r.rows){
 						var defaultExport = input.attr('data-default');
-						var selectExport = $('#exportModel');
 						selectExport.find('option').remove();
 						$.each(r.rows,function(i, option){
-							var opt = $('<option value="'+option.id+'">'+option.label+' - '+option.description+'</option>');
+							var opt = $('<option value="'+option.id+'">'+option.label+(option.description.length?' - '+option.description:'')+'</option>');
 							if(defaultExport && defaultExport.length && option.slug === defaultExport) opt.attr('selected', true);
 							selectExport.append(opt);
 						});
 					}
 					$('#exportmodel-form').attr('data-parameters', JSON.stringify(parameters));
-					if($('#exportModel > option').length == 1){
-						export_exportmodel_export();
+					
+					if($('> option',selectExport).length == 1){
+						selectExport.val($('>option',selectExport).val());
+						if(selectExport.val()=='none'){
+							$.message('warning', "Aucun modèle d'export existant");
+							return;
+						}
+						input.find('.btn').addClass('btn-preloader');
+						export_model_export(postCallback);
 						return;
 					}
 					init_components($('#export-modal'));
 					modal.modal('show');
+					if(postCallback!=null) modal.attr('data-post-callback', postCallback);
 				});
 			});
 		});

+ 141 - 102
plugin/export/js/main.js

@@ -3,8 +3,8 @@ var isProcessing = false;
 function init_plugin_export(){
 	switch($.urlParam('page')){
 		case 'sheet':
-			// export_exportmodel_list_dataset();
-			export_exportmodel_get_dataset($('#dataset'));
+			export_model_get_dataset($('#dataset'));
+			export_model_format_refresh();
 		break;
 		default:
 		break;
@@ -18,17 +18,18 @@ function init_setting_export(parameter){
 		break;
 	}
 	$('#exportmodels').sortable_table({
-		onSort : export_exportmodel_search
+		onSort : export_model_search
 	});
 }
 
-
 /* EXPORTMODELE */
 //Récuperation d'une liste de exportmodel dans le tableau #exportmodels
-function export_exportmodel_search(callback){
+function export_model_search(callback){
+	var box = new FilterBox('#filters');
+
 	$('#exportmodels').fill({
-		action:'export_exportmodel_search',
-		filters : $('#filters').filters(),
+		action:'export_model_search',
+	    filters : box.filters(),
 		sort : $('#exportmodels').sortable_table('get')
 	}, function(){
 		if(callback!=null) callback();
@@ -36,24 +37,47 @@ function export_exportmodel_search(callback){
 }
 
 //Ajout ou modification d'élément exportmodel
-function export_exportmodel_save(){
+function export_model_save(){
 	var data = $('#exportmodel-form').toJson();
+	data.plugin = $('#dataset option:selected').attr('data-plugin');
 	$.action(data,function(r){
 		$('#exportmodel-form').attr('data-id',r.id);
+		$.urlParam('id',r.id);
+		
 		$('#document_temporary').val('');
-		$('#exportmodel-form [data-type="dropzone"] ul > li').attr('data-path', r.relativePath);
-		$('#exportmodel-form [data-type="dropzone"] ul > li > a').attr('href', r.filePath);
-		$('#exportmodel-form [data-type="dropzone"] ul > li > i.fa-times').attr('onclick', 'export_exportmodel_delete_document(this)');
-		$('.options-box').load(document.URL +  ' .options-box>*');
+		var modelFile = $('#exportmodel-form [data-type="dropzone"] ul > li');
+		modelFile.attr('data-path', r.relativePath);
+		$('> a',modelFile).attr('href', r.filePath);
+		$('> i.fa-times',modelFile).attr('onclick', 'export_model_delete_document(this)');
+		
 		$.message('success','Export modèle enregistré');
 	});
 }
 
+function export_model_format_refresh(){
+	var li = $('#document > ul > li:visible');
+	var tpl = '<option  value="{{uid}}">{{label}}</option>';
+	$('#export_format').html('');
+	$('.options-box').addClass('hidden');
+	if(li.length==0 || li.attr('data-ext')=='') return;
+
+	$.action({
+		action: 'export_model_format_refresh',
+		ext: li.attr('data-ext')
+	},function(r){
+		for(var key in r.rows)
+			$('#export_format').append(Mustache.render(tpl,r.rows[key]));
+		
+		$('#export_format').val( ($('#export_format').attr('data-value').length ? $('#export_format').attr('data-value') : li.attr('data-ext')) );
+		if(r.rows.length>0) $('.options-box').removeClass('hidden');
+	});
+}
+
 //Récuperation ou edition d'élément exportmodel
-function export_exportmodel_edit(element){
+function export_model_edit(element){
 	var line = $(element).closest('tr');
 	$.action({
-		action:'export_exportmodel_edit',
+		action:'export_model_edit',
 		id:line.attr('data-id')
 	},function(r){
 		$.setForm('#exportmodel-form',r);
@@ -62,11 +86,11 @@ function export_exportmodel_edit(element){
 }
 
 //Suppression d'élement exportmodel
-function export_exportmodel_delete(element){
+function export_model_delete(element){
 	if(!confirm('Êtes-vous sûr de vouloir supprimer cet item ?')) return;
 	var line = $(element).closest('tr');
 	$.action({
-		action : 'export_exportmodel_delete',
+		action : 'export_model_delete',
 		id : line.attr('data-id')
 	},function(r){
 		line.remove();
@@ -75,15 +99,15 @@ function export_exportmodel_delete(element){
 }
 
 //Ajout de document
-function export_exportmodel_add_document(files){
+function export_model_add_document(files){
 	var form = $('#exportmodel-form');
 	var exportmodelId = form.attr('data-id');
 
 	$.action({
-		action : 'export_exportmodel_add_document',
+		action : 'export_model_add_document',
 		id: exportmodelId,
 		files : files,
-		plugin: $('#plugin').val()
+		plugin: $('#dataset option:selected').attr('data-plugin')
 	}, function(r){
 		form.attr('data-id', r.id);
 
@@ -91,57 +115,33 @@ function export_exportmodel_add_document(files){
 			var line = $('li[data-path="'+file.oldPath+'"]', form);
 			line.attr('data-path', file.relative);
 			line.find('a').attr('href', file.url);
-			line.find('i.pointer').attr('onclick', 'export_exportmodel_delete_document(this)');
+			line.find('i.pointer').attr('onclick', 'export_model_delete_document(this)');
 
 			$('[data-type="dropzone"] input:not(:visible)', form).val('');
 			
 			$.message('success', 'Fichier "'+file.name+'" sauvegardé');
 		});
-		$('.options-box').load(document.URL +  ' .options-box>*');
+		export_model_format_refresh();
 	});
 }
 
 //Suppression de document
-function export_exportmodel_delete_document(element){
+function export_model_delete_document(element){
 	if(!confirm("Êtes-vous sûr de vouloir supprimer ce fichier ?")) return;
 	var line = $(element).closest('li');
 	$.action({
-		action : 'export_exportmodel_delete_document',
+		action : 'export_model_delete_document',
 		path : line.attr('data-path')
 	},function(r){
-		$('.options-box').load(document.URL +  ' .options-box>*');
 		line.remove();
+		export_model_format_refresh();
 		$.message('info','Élement supprimé');
 	});
 }
 
-//Permet de récupérer les jeux de 
-//données associés au plugin choisi
-function export_exportmodel_list_dataset(){
-	var parameters = {
-		plugin: $('#plugin').val()
-	}
-
-	$.action({
-		action: 'export_exportmodel_list_dataset',
-		params: parameters
-	}, function(r){
-		var datasetSelect = $('#dataset');
-		var tpl = datasetSelect.find('option.hidden').get(0).outerHTML;
-		datasetSelect.find('option:not(.template)').remove();
-		datasetSelect.val('');
-		$.each(r.rows, function(slug, dataset){
-			var option = $(Mustache.render(tpl, dataset));
-			option.removeClass('hidden template');
-			datasetSelect.find('option.hidden').before(option);
-		});
-		export_exportmodel_get_dataset($('#dataset'));
-	});
-}
-
 //Récupération du jeu de données 
 //associé à l'élément exportmodel
-function export_exportmodel_get_dataset(element){
+function export_model_get_dataset(element){
 	var container = $('#dataset-container');
 	container.addClass('hidden');
 	$('#empty-files').removeClass('hidden');
@@ -149,76 +149,115 @@ function export_exportmodel_get_dataset(element){
 
 	var datasetSelect = $(element);
 	var curVal = datasetSelect.val();
-	var pluginVal = $('#plugin').val();
+	var pluginVal = $('option:selected',datasetSelect).attr('data-plugin');
 
 	if(!curVal || !pluginVal) return;
 
-	var parameters = {
-		dataset: curVal,
-		plugin: pluginVal
+	var label = $('#label');
+	if(!label.length) label.val(pluginVal+': '+$('option:selected',datasetSelect).text());
+
+	if($('#slug').length && !$('#slug').val().length){
+		var slug = label.val().toString().toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/\-\-+/g, '-');
+		$('#slug').val(slug);
 	}
-	
+
 	$.action({
-		action: 'export_exportmodel_get_dataset',
-		params: parameters
+		action: 'export_model_get_dataset',
+		params: {
+			dataset: curVal,
+			plugin: pluginVal
+		}
 	}, function(r){
 		container.removeClass('hidden');
 		if(r.dataset){
 			var showcase = $('.dataset-showcase');
 			var tpl = $('li.hidden',showcase).get(0).outerHTML;
-			$.each(r.dataset, function(macro, info){
-				var datas = {
-					macro: (info.type == 'list')?'{{#'+macro+'}}{{/'+macro+'}}':'{{'+macro+'}}',
-					description:info.desc,
-					type:info.type,
-					badge: (info.type == 'list')?'Liste':'Valeur'
-				};
-				if(info.type == 'photo') datas.badge = 'Photo';
-
-				var li = $(Mustache.render(tpl, datas));
-				if(info.subitems) {
-					var ul = $('<ul class="subitems"></ul>');
-					$.each(info.subitems, function(tag, val){
-						var subDatas = {
-							macro: '{{'+tag+'}}',
-							description: val !== null && typeof val === 'object' ? val.desc : val,
-							type: val.type ? val.type : 'value',
-							badge: val.type == 'photo' ? 'Photo' : 'Valeur'
-						};
-						var subLi = $(Mustache.render(tpl, subDatas));
-						subLi.removeClass('hidden template');
-						ul.append(subLi);
-					});
-					li.append(ul);
-				}
-				li.removeClass('hidden template');
-				$('> ul', showcase).append(li);
-			});
+			export_model_recursive_dataset($('> ul', showcase),tpl,r.dataset,'');
 		}
 
-		if(r.files && r.files.length){
-			var exampleFiles = $('.dataset-example-files');
-			$('#empty-files').addClass('hidden');
-			var aTpl = $('li.hidden',exampleFiles).get(0).outerHTML;
-			$.each(r.files, function(i, file){
-				var a = $(Mustache.render(aTpl, file));
-				a.removeClass('hidden template');
-				$('ul', exampleFiles).append(a);
-			});
+		if(r.files && r.files.length)
+			$('.dataset-example-files > ul').addLine(r.files);
+	});
+}
+
+function export_copy_macro(element,event){
+	event.stopPropagation();
+	var element = $(element);
+
+	value = export_get_macro_recursive(element, []); 
+	value.reverse();
+	if(element.attr('data-type')!='list') value = '{{'+value.join('.').replace(/[}{]/ig,'')+'}}';
+	
+	copy_string(value,element.find('>code'));
+}
+
+function export_get_macro_recursive(element,path){
+	var parent = element.parent().parent();
+	if(parent.attr('data-macro')!=null && parent.attr('data-macro')!='' && parent.attr('data-type')!='list'){
+		path.push(element.attr('data-macro'));
+		path.concat(export_get_macro_recursive(parent,path));
+	}else{
+		path.push(element.attr('data-macro'));
+	}
+	return path;
+}
+
+function export_model_recursive_dataset(container,tpl,dataset){
+	var typeLabel = {
+		'list' : 'Liste',
+		'image' : 'Image',
+		'value': 'Valeur',
+		'object': 'Objet'
+	};
+
+	$.each(dataset, function(macro, data){
+		data.macro = data.type == 'list' ?'{{#'+macro+'}}{{/'+macro+'}}':'{{'+macro+'}}';
+		data.type = data.type && data.type!=''  ?  data.type  : 'value';
+		data.badge = typeLabel[data.type];
+		var li = $(Mustache.render(tpl, data));
+
+		li.removeClass('hidden template');
+		container.append(li);
+
+		if(data.type=='object' || data.type=='list'){
+			var ul = $('<ul class="subitems"></ul>');
+			li.append(ul);
+			if(data.type=='list'){
+				data.value = data.value[0];
+			}
+			export_model_recursive_dataset(ul,tpl,data.value);
 		}
+
 	});
 }
 
-//Suppression de document
-function export_exportmodel_export(element){
+
+//Export des données en fonction du modèle
+//sélectionné dans la modal d'export
+function export_model_export(callback){
 	if(isProcessing) return;
-	var exportmodelId = $('#exportModel').val();
-	var parameters = $('#exportmodel-form').attr('data-parameters');
-	var url = window.location.href;
-	var previousUrl = url.substring(url.lastIndexOf('/')+1);
+	var modal = $('#export-modal');
+	var model = $('#exportModel').val();
+	if(model=='none') return $.message('error', 'Vous devez choisir un modèle d\'export');
+	callback = callback!=null ? callback : modal.attr('data-post-callback');
+
+	var parameters = JSON.parse($('#exportmodel-form').attr('data-parameters'));
+	parameters.model = model;
+	parameters.previousUrl = window.location.href.substring(window.location.href.lastIndexOf('/')+1);
 
 	isProcessing = true;
-	var exportUrl = 'action.php?action=export_exportmodel_export&exportmodel=';
-	window.location = exportUrl+btoa(exportmodelId)+'&params='+btoa(parameters)+'&url='+btoa(previousUrl);
-	isProcessing = false;
+	$('#export-button', modal).addClass('btn-preloader');
+	$.action({
+		action: 'export_model_export',
+		parameters: parameters,
+		downloadResponse: !("destination" in parameters)
+	}, function(r){
+		isProcessing = false;
+		$.message('success', "Export modèle correctement généré");
+		modal.modal('hide');
+
+		if(callback!=null && callback.length) window[callback](r);
+	}, function(r){
+		isProcessing = false;
+	});
 }

+ 12 - 11
plugin/export/modal.export.model.php

@@ -2,26 +2,27 @@
 	<div class="modal-dialog modal-lg" role="document">
 		<div class="modal-content">
 			<div class="modal-header bg-bordeaux">
-				<h5 class="modal-title" id="export-modal-label">Export modèle :</h5>
+				<h4 class="modal-title" id="export-modal-label">Export modèle :</h4>
 				<button type="button" class="close" data-dismiss="modal" aria-label="Fermer">
 					<span aria-hidden="true">&times;</span>
 				</button>
 			</div>
-			<div class="modal-body">
-				<div class="navigation">
-					<div id="exportmodel-form" class="row exportmodel-form" data-action="export_exportmodel_export" data-id="">
-						<div class="col-md-12">
-							<h4>Choix du modèle d'export :</h4>
-							<select name="exportModel" id="exportModel" class="form-control"></select>
-							<br>
-							<a href="index.php?module=export&page=sheet" target="_blank"><i class="fas fa-plus"></i> Créer un nouvel export modèle</a>
-						</div>
+			<div class="modal-body row">
+				<div id="exportmodel-form" class="col-md-12 exportmodel-form" data-action="export_model_export" data-id="">
+					<legend>Choisissez un modèle d'export :</legend>
+					<div class="input-group mb-3">
+						<select name="exportModel" id="exportModel" class="form-control">
+							<option value="none"> - </option>
+						</select>
+						<div class="input-group-append">
+						    <a class="btn btn-outline-secondary" href="index.php?module=export&page=sheet" target="_blank" title="Créer un nouvel export modèle"><i class="fas fa-plus"></i></a>
+						  </div>
 					</div>
 				</div>
 			</div>
 			<div class="modal-footer">
 				<div class="btn btn-light" data-dismiss="modal">Fermer</div>
-				<div onclick="export_exportmodel_export();" class="btn btn-success"><i class="fas fa-file-export"></i> Exporter</div>
+				<div onclick="export_model_export();" class="btn btn-success" id="export-button"><i class="fas fa-file-export"></i> Exporter</div>
 			</div>
 		</div>
 	</div>

+ 70 - 0
plugin/export/page.documentation.php

@@ -0,0 +1,70 @@
+<?php
+User::check_access('export','configure');
+require_once(__DIR__.SLASH.'ExportModel.class.php');
+
+?>
+<div class="row justify-content-md-center">
+	<div class="col-md-8">
+		<legend>Documentation :</legend>
+		<section class="export-model-documentation">
+			<div>
+				<span class="mb-2 block">Vous pouvez créer de nouveaux modèles d'exports avec l'une des extensions suivantes :</span>
+				<ul>
+					<?php foreach(ExportModel::templates() as $exportModel): ?>
+					<li><code>.<?php echo $exportModel['extension']; ?> </code> <small>(<?php echo $exportModel['description']; ?>)</small></li>
+					<?php endforeach; ?>
+				</ul>
+				Des fichiers d'exemple à télécharger vous sont proposés dans chacun de ces formats.
+			</div>
+			<div>
+				À l'intérieur de ces documents vous pouvez utiliser des <strong>macros</strong> qui représenteront les <i>informations à exporter</i>.<br>
+				Par exemple, <code>{{utilisateur.identifiant}}</code> sera remplacé lors de l'export par votre identifiant de connexion.
+			</div>
+			<div>
+				<span class="mb-2 block">Les macros peuvent retourner trois types d'informations :</span>
+				<ul>
+					<li>
+						Le type <span class="badge macro-value">Valeur</span>:<br>
+						<i>ex :</i> <code>{{utilisateur.prénom}}</code> retourne <strong>John</strong>
+					</li>
+					<li>Le type <span class="badge macro-list">Liste</span>:<br>
+						<i>ex :</i> <code><strong>{{#liste.utilisateurs}}</strong>{{utilisateur.prénom}} {{utilisateur.nom}},<strong>{{/liste.utilisateurs}}</strong></code>
+						retourne la liste des prénoms et noms des utilisateurs du logiciel séparés par virgules.
+					</li>
+					<li>
+						Le type <span class="badge macro-image">Photo</span>:<br>
+						<i>ex :</i> <code>{{utilisateur.photo}}</code> retourne la photo <img src="img/default-avatar.png" alt="Image de profil par défaut" class="doc-picture avatar-rounded">
+					</li>
+				</ul>
+			</div>
+			<div>
+				<span class="mb-2 block">
+					Il y a également la possibilité de faire des conditions sur certains champs.<br>
+					Ces conditions se présentent sous la forme d'un <b>Si</b> <code>&lt;condition&gt;</code> <b>Alors</b> <i>&lt;résultat&gt;</i> <b>Sinon</b> <i>&lt;autre-résultat&gt;</i><br><br>
+					Pour utiliser les conditions vous devez :
+				</span>
+				<ol>
+					<li>
+						Ouvrir la macro de condition <b>Si</b> : <code>{{#condition}}</code>
+					</li>
+					<li>
+						Placer le <i>contenu</i> <code></code>
+					</li>
+					<li>
+						Fermer la macro de condition : <code>{{/condition}}</code>
+					</li>
+					<li>
+						Ouvrir la macro de condition <b>Sinon</b> : <code>{{^condition}}</code>
+					</li>
+					<li>
+						Placer le <i>contenu</i> <code></code>
+					</li>
+					<li>
+						Fermer la macro de condition : <code>{{/condition}}</code>
+					</li>
+				</ol>
+				Par ailleurs, il est aussi possible de n'utiliser que la condition <b>Si</b> comme ceci : <code>{{#condition}}</code> <i>contenu</i> <code>{{/condition}}</code>
+			</div>
+		</section>			
+	</div>
+</div>

+ 54 - 127
plugin/export/page.sheet.php

@@ -3,171 +3,98 @@ User::check_access('export','configure');
 require_once(__DIR__.SLASH.'ExportModel.class.php');
 $exportmodel = ExportModel::provide();
 
+$datasets = array();
+Plugin::callHook('export_model_data', array(&$datasets, array() ));
+
+$dataSetPicker = array();
+foreach ($datasets as $slug=>$dataset) {
+	if(!isset($dataSetPicker[$dataset['plugin']])) $dataSetPicker[$dataset['plugin']] = array();
+	$dataSetPicker[$dataset['plugin']][] = $dataset;
+} 
 ?>
 <br>
 <div class="export">
-	<div id="exportmodel-form" class="row exportmodel-form" data-action="export_exportmodel_save" data-id="<?php echo $exportmodel->id; ?>">
+	<div id="exportmodel-form" class="row exportmodel-form" data-action="export_model_save" data-id="<?php echo $exportmodel->id; ?>">
 		<div class="col-md-12">
 			<h3 class="d-inline-block">Fiche export modèle</h3>
-			<div onclick="export_exportmodel_save();" class="btn btn-success float-right"><i class="fas fa-check"></i> Enregistrer</div>
-			<div onclick="window.history.back();" class="btn btn-info float-right mr-2"><i class="fas fa-arrow-left"></i> Retour</div>
+			<div onclick="export_model_save();" class="btn btn-success float-right"><i class="fas fa-check"></i> Enregistrer</div>
+			<a href="setting.php?section=export" class="btn btn-info float-right mr-2"><i class="fas fa-arrow-left"></i> Retour</a>
 			<hr>
 		</div>
 		<div class="col-md-8">
-			<div class="row">
-				<div class="<?php echo $myUser->superadmin ? 'col-md-8' : 'col-md-12'; ?>">
+			<div class="row mb-2">
+				<div class="col-md-8">
 					<label for="label">Libellé : </label>
 					<input required id="label" name="label" class="form-control" placeholder="Nom export modèle" value="<?php echo html_decode_utf8($exportmodel->label); ?>" type="text">
 				</div>
-				<?php if($myUser->superadmin): ?>
-				<div class="col-md-4">
-					<label for="label">Slug : </label>
-					<input required id="slug" name="slug" class="form-control" placeholder="Slug export modèle" value="<?php echo html_decode_utf8($exportmodel->slug); ?>" type="text">
-				</div>
-				<?php endif; ?>
-				<div class="col-md-12"><br>
-					<label for="description">Description : </label>
-					<input id="description" name="description" class="form-control" placeholder="Description export modèle" value="<?php echo html_decode_utf8($exportmodel->description); ?>" type="text">
+				<div class="col-md-4 position-relative">
+					<div class="export-privacy-button text-center">
+						<label for="privacy" class="pointer"><i class="fas fa-user-secret"></i>&nbsp;Export modèle privé :</label>
+						<input type="checkbox" data-type="checkbox" id="privacy" name="privacy" <?php echo $exportmodel->privacy==ExportModel::PRIVACY_PRIVATE?'checked':''; ?>>
+					</div>
 				</div>
-			</div><br>
+			</div>
 			<div class="row">
-				<div class="col-md-4">
-					<label for="plugin">Plugin : </label>
-					<select required id="plugin" name="plugin" class="form-control" onchange="export_exportmodel_list_dataset();">
-						<option value=""> - </option>
-						<?php foreach (Plugin::getAll(true) as $plugin) {
-							$selected = $plugin->folder == $exportmodel->plugin ? 'selected' : '';
-							echo '<option '.$selected.' value="'.$plugin->folder.'">'.$plugin->name.'</option>';
-						} ?>
-					</select>
-				</div>
-				<div class="col-md-4">
-					<label for="dataset">Jeu de données : </label>
-					<select required name="dataset" id="dataset" class="form-control" onchange="export_exportmodel_get_dataset(this);">
+				<div class="<?php echo $myUser->superadmin ? 'col-md-8' : 'col-md-12'; ?>">
+					<label for="dataset">Jeu de données associé au modèle d'export : </label>
+					<select required name="dataset" id="dataset" class="form-control" onchange="export_model_get_dataset(this);">
 						<option value="" class="template"> - </option>
-						<option value="{{dataset}}" class="hidden template">{{label}}</option>
-						<?php 
-							if(isset($exportmodel->plugin) && !empty($exportmodel->plugin)){
-								$datasets = array();
-								Plugin::callHook('export_model_data', array(&$datasets, array('plugin'=>$exportmodel->plugin)));
-								foreach($datasets as $slug => $dataset) {
-									$selected = $slug == $exportmodel->dataset ? 'selected' : '';
-									echo '<option '.$selected.' value="'.$dataset['dataset'].'">'.$dataset['label'].'</option>';
-								}
-							}
-						?>
+						<?php foreach ($dataSetPicker as $plugin=>$datasets): ?>
+							<optgroup label="<?php echo $plugin; ?>">
+								<?php foreach ($datasets as $dataset): ?>
+									<option <?php echo $exportmodel->dataset==$dataset['dataset'] ? 'selected="selected"':'' ?> data-plugin="<?php echo $plugin; ?>" value="<?php echo $dataset['dataset']; ?>"><?php echo $dataset['label']; ?></option>
+								<?php endforeach; ?>
+							</optgroup>
+						<?php endforeach; ?>
 					</select>
 				</div>
-				<div class="col-md-4 position-relative">
-					<div class="export-privacy-button text-center">
-						<label for="privacy" class="pointer"><i class="fas fa-user-secret"></i>&nbsp;Export modèle privé :</label>
-						<input type="checkbox" data-type="checkbox" id="privacy" name="privacy" <?php echo $exportmodel->privacy==ExportModel::PRIVACY_PRIVATE?'checked':''; ?>>
+				<?php if($myUser->superadmin): ?>
+					<div class="col-md-4">
+						<label for="label">Slug : </label>
+						<input required id="slug" name="slug" class="form-control" placeholder="Slug export modèle" value="<?php echo html_decode_utf8($exportmodel->slug); ?>" type="text">
 					</div>
+				<?php endif; ?>
+				<div class="col-md-12 mt-2">
+					<label for="description">Description : </label>
+					<input id="description" name="description" class="form-control" placeholder="Description export modèle" value="<?php echo html_decode_utf8($exportmodel->description); ?>" type="text">
 				</div>
 			</div>
-			<br/>
 		</div>
+
 		<div class="col-md-4">
 			<div>
 				<label for="document">Fichier modèle :</label>
-				<div data-type="dropzone" data-allowed="<?php echo implode(',',array_keys(ExportModel::templates())); ?>" data-label="Faites glisser le modèle d'import ici..." data-delete="export_exportmodel_delete_document" data-save="export_exportmodel_add_document" class="form-control" id="document" name="document"><?php echo json_encode($exportmodel->documents()); ?></div>
+				<div data-type="dropzone" data-max-files="1" data-allowed="<?php echo implode(',',array_keys(ExportModel::templates())); ?>" data-label="Faites glisser le modèle d'export ici..." data-delete="export_model_delete_document" data-save="export_model_add_document" class="form-control" id="document" name="document"><?php echo json_encode($exportmodel->documents()); ?></div>
 			</div>
-
+	
 			<div class="options-box">
-				<?php if(in_array($exportmodel->export_format, array('html','pdf'))): ?>
-				<div class="">
-					<label for="pdf" class="pointer">&nbsp;Export au format :</label>
-					<select name="export_format" id="export_format" class="form-control">
-						<?php foreach(array('html', 'pdf') as $format): ?>
-							<option value="<?php echo $format; ?>" <?php echo $exportmodel->export_format==$format?'selected':''; ?>><?php echo mb_strtoupper($format); ?></option>
-						<?php endforeach; ?>
-					</select>
-				</div>
-				<?php endif; ?>
+				<!-- format box -->
+				<label for="export_format" class="pointer">&nbsp;Export au format :</label>
+				<select name="export_format" data-value="<?php echo $exportmodel->export_format; ?>" id="export_format" class="form-control"></select>
 			</div>
 		</div>
-		<hr class="col-md-12">
 		<div class="clear"></div>
-		<div id="dataset-container" class="col-md-12 dataset-container hidden">
+		<hr class="col-md-12">
+		<div id="dataset-container" class="col-md-12 dataset-container p-0 hidden">
 			<div class="row">
-				<div class="col-md-9 dataset-showcase">
-					<span class="mb-2 block">Liste des macros disponibles pour ce jeu de données :</span>
+				<div class="col-md-8 dataset-showcase">
+					<h5 class="mb-2 d-inline-block">Liste des macros disponibles pour ce jeu de données :</h5>
+					<a class="d-inline-block float-right" href="index.php?module=export&page=documentation"><i class="far fa-question-circle"></i> Voir la documentation générale</a>
+					<div class="clear"></div>
 					<ul>
-						<li onclick="select_text($(this).find('>code'), event);copy_to_clipboard($(this).find('>code'));" class="hidden template li-{{type}}"><span class="badge macro-{{type}}">{{badge}}</span><code>{{macro}}</code> : {{description}}</li>
+						<li onclick="export_copy_macro(this,event)" class="hidden template li-{{type}}" data-type="{{type}}" data-macro="{{macro}}">
+							<span class="badge badge-pill macro-{{type}}">{{badge}}</span>
+							<code data-code="{{absolutemacro}}">{{macro}}</code> : {{label}}
+						</li>
 					</ul>
 				</div>
-				<div class="col-md-3 dataset-example-files">
+				<div class="col-md-4 dataset-example-files">
 					<span class="mb-2 block"><i class="fas fa-cloud-download-alt"></i> Fichiers d'exemple à télécharger :</span>
 					<ul>
-						<li class="template" id="empty-files">Aucun fichier</li>
 						<li class="hidden template" title="Cliquer pour copier dans le presse-papier"><a href="{{path}}" class="font-weight-bold"><i class="fas {{icon}}"></i> {{label}}</a></li>
 					</ul>
 				</div>
 			</div>
-			<hr class="col-md-12">
-		</div>
-		<div class="col-md-12">
-			<h4>Documentation</h4>
-			<section class="export-doc">
-				<div>
-					<span class="mb-2 block">Vous pouvez créer de nouveaux modèles d'exports avec l'une des extensions suivantes :</span>
-					<ul>
-						<?php foreach(ExportModel::templates() as $exportModel): ?>
-						<li><code>.<?php echo $exportModel['extension']; ?> </code> <small>(<?php echo $exportModel['description']; ?>)</small></li>
-						<?php endforeach; ?>
-					</ul>
-					Des fichiers d'exemple à télécharger vous sont proposés dans chacun de ces formats.
-				</div>
-				<div>
-					À l'intérieur de ces documents vous pouvez utiliser des <strong>macros</strong> qui représenteront les <i>informations à exporter</i>.<br>
-					Par exemple, <code>{{utilisateur.identifiant}}</code> sera remplacé lors de l'export par votre identifiant de connexion.
-				</div>
-				<div>
-					<span class="mb-2 block">Les macros peuvent retourner trois types d'informations :</span>
-					<ul>
-						<li>
-							Le type <span class="badge macro-value">Valeur</span>:<br>
-							<i>ex :</i> <code>{{utilisateur.prénom}}</code> retourne <strong>John</strong>
-						</li>
-						<li>Le type <span class="badge macro-list">Liste</span>:<br>
-							<i>ex :</i> <code><strong>{{#liste.utilisateurs}}</strong>{{utilisateur.prénom}} {{utilisateur.nom}},<strong>{{/liste.utilisateurs}}</strong></code>
-							retourne la liste des prénoms et noms des utilisateurs du logiciel séparés par virgules.
-						</li>
-						<li>
-							Le type <span class="badge macro-photo">Photo</span>:<br>
-							<i>ex :</i> <code>{{utilisateur.photo}}</code> retourne la photo <img src="img/default-avatar.png" alt="Image de profil par défaut" class="doc-picture avatar-rounded">
-						</li>
-					</ul>
-				</div>
-				<div>
-					<span class="mb-2 block">
-						Il y a également la possibilité de faire des conditions sur certains champs.<br>
-						Ces conditions se présentent sous la forme d'un <b>Si</b> <code>&lt;condition&gt;</code> <b>Alors</b> <i>&lt;résultat&gt;</i> <b>Sinon</b> <i>&lt;autre-résultat&gt;</i><br><br>
-						Pour utiliser les conditions vous devez :
-					</span>
-					<ol>
-						<li>
-							Ouvrir la macro de condition <b>Si</b> : <code>{{#condition}}</code>
-						</li>
-						<li>
-							Placer le <i>contenu</i> <code></code>
-						</li>
-						<li>
-							Fermer la macro de condition : <code>{{/condition}}</code>
-						</li>
-						<li>
-							Ouvrir la macro de condition <b>Sinon</b> : <code>{{^condition}}</code>
-						</li>
-						<li>
-							Placer le <i>contenu</i> <code></code>
-						</li>
-						<li>
-							Fermer la macro de condition : <code>{{/condition}}</code>
-						</li>
-					</ol>
-					Par ailleurs, il est aussi possible de n'utiliser que la condition <b>Si</b> comme ceci : <code>{{#condition}}</code> <i>contenu</i> <code>{{/condition}}</code>
-				</div>
-			</section>
 		</div>
 	</div>
 </div><br>

+ 16 - 22
plugin/export/setting.export.php

@@ -8,7 +8,7 @@ foreach ($plugins as $plugin)
 	$allPlugs[$plugin->folder] = $plugin->name;
 
 ?>
-<div class="row">
+<div class="row mb-2">
 	<div class="col-md-12">
 		<br>
 		<?php if($myUser->can('export', 'edit')) : ?>
@@ -19,7 +19,7 @@ foreach ($plugins as $plugin)
 	</div>
 
     <div class="col-md-12">
-        <select id="filters" data-slug="exportmodel-search" data-join="and" data-type="filter" data-label="Recherche" data-function="export_exportmodel_search">
+        <select id="filters" data-slug="exportmodel-search" data-type="filter" data-label="Recherche" data-function="export_model_search">
             <option value="description" data-filter-type="text">Description</option>
             <option value="filename" data-filter-type="text">Nom de fichier</option>
             <option value="plugin" data-filter-type="dictionnary" data-filter-source='<?php echo json_encode($allPlugs); ?>'>Plugin</option>
@@ -27,40 +27,34 @@ foreach ($plugins as $plugin)
         </select>
     </div>
 </div>
-<br>
-
 <div class="row">
 	<!-- search results -->
 	<div class="col-xl-12">
-		<table id="exportmodels" class="table table-striped table-exportmodels " data-entity-search="export_exportmodel_search">
+		<table id="exportmodels" class="table table-striped table-exportmodels " data-entity-search="export_model_search">
             <thead>
                 <tr>
-                    <th data-sortable="id">#</th>
+                    <th></th>
                     <th data-sortable="label">Libellé</th>
-                    <?php if($myUser->superadmin): ?>
-                    <th data-sortable="slug">Slug</th>
-                    <?php endif; ?>
                     <th data-sortable="description">Description</th>
                     <th data-sortable="plugin">Plugin</th>
                     <th style="width: 150px;">Jeu de données</th>
-                    <th data-sortable="filename">Nom de fichier</th>
+                   
                 	<th></th>
                 </tr>
             </thead>
             <tbody>
                 <tr data-id="{{id}}" class="hidden">
-                    <td>{{id}}</td>
-	                <td class="exportmodel-{{class}}" title="Export modèle {{privacy}}">{{label}}</td>
-                    <?php if($myUser->superadmin): ?>
-                    <td><code>{{slug}}</code></td>
-                    <?php endif; ?>
-	                <td>{{description}}</td>
-	                <td>{{pluginName}}</td>
-	                <td>{{datasetName}}</td>
-	                <td>{{filename}}</td>
-	                <td class="text-right">
+                    <td class="align-middle p-2"><h2 class="text-center m-0"><i class="{{icon}}"></i></h2></td>
+	                <td class="exportmodel-{{class}} align-middle p-2" title="Export modèle {{privacy}} ({{slug}})">
+                        <a class="text-primary" href="index.php?module=export&page=sheet&id={{id}}">{{label}}</a>
+                        <div class="text-muted"><small>Modèle : {{filename}}</small></div>
+                    </td>
+	                <td class="align-middle p-2">{{description}}</td>
+	                <td class="align-middle p-2">{{pluginName}}</td>
+	                <td class="align-middle p-2">{{datasetName}}</td>
+	                <td class="text-right align-middle p-2">
 	                    <a class="btn btn-info btn-squarred btn-mini" href="index.php?module=export&page=sheet&id={{id}}"><i class="fas fa-pencil-alt"></i></a>
-	                    <div class="btn btn-danger btn-squarred btn-mini" onclick="export_exportmodel_delete(this);"><i class="fas fa-times"></i></div>
+	                    <div class="btn btn-danger btn-squarred btn-mini" onclick="export_model_delete(this);"><i class="fas fa-times"></i></div>
 	                </td>
                 </tr>
            </tbody>
@@ -68,7 +62,7 @@ foreach ($plugins as $plugin)
 
         <!-- Pagination -->
         <ul class="pagination justify-content-center">
-            <li class="page-item hidden" data-value="{{value}}" title="Voir la page {{label}}" onclick="$(this).parent().find('li').removeClass('active');$(this).addClass('active');export_exportmodel_search()">
+            <li class="page-item hidden" data-value="{{value}}" title="Voir la page {{label}}" onclick="$(this).parent().find('li').removeClass('active');$(this).addClass('active');export_model_search()">
                 <span class="page-link">{{label}}</span>
             </li>
         </ul>

+ 18 - 5
plugin/export/template/CsvExport.class.php

@@ -1,12 +1,25 @@
 <?php
 require_once(__DIR__.SLASH.'TextExport.class.php');
 class CsvExport extends TextExport{
-	public static $mime = 'application/csv';
-	public static $extension = 'csv';
-	public static $description = 'Fichier tableur générique';
+	public $mime = 'application/csv';
+	public $extension = 'csv';
+	public $description = 'Fichier tableur générique';
 
-	public static function sample($dataset){
-		return utf8_decode(parent::sample($dataset));
+	public  function from_template($stream, $datas){
+		return parent::from_template($stream, $datas);
+	}
+
+	public function end($stream){
+		return  utf8_decode($stream);
+	}
+
+	public  function sample($dataset,$level = 0,$parent=''){
+		return utf8_decode(parent::sample($dataset,$level));
+	}
+
+	public  function formatValue($type,$value){
+		if($type == 'image') return '';
+		return $value;
 	}
 }
 ?>

+ 276 - 165
plugin/export/template/ExcelExport.class.php

@@ -2,25 +2,66 @@
 
 require_once (LIB_PATH.'XLSXWriter'.SLASH.'XLSXWriter.class.php');
 
-class ExcelExport
-{
-
-	public static $mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
-	public static $extension = 'xlsx';
-	public static $description = 'Fichier classeur de données Excel';
-
-	public static function sample($dataset){
-		foreach($dataset as $macro => $infos) {
-			$data[]['Macros disponibles :'] = ($infos['type']=='list') ? '{{#'.$macro.'}}{{/'.$macro.'}} : '.$infos['desc'] : '{{'.$macro.'}} : '.$infos['desc'];
-		}
+class ExcelExport{
+	public $mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
+	public $extension = 'xlsx';
+	public $description = 'Fichier classeur de données Excel';
+	public $tempFiles = array();
+	public $source = '';
+	public $spreadsheet;
+
+	public  function sample($dataset, $level=0, $parent=''){
+		$data = $this->recursive_sample($dataset,$level ,$parent);
 		$stream = Excel::exportArray($data, null ,'Sans titre');
 		return  $stream;
 	}	
 
+	public function recursive_sample($dataset, $level=0, $parent=''){
+		$stream = array(); 
+		$parent = ($parent!=''?$parent.'.':'');
+		$indentation = str_repeat("\t", $level);
+		foreach($dataset as $macro => $infos){
+			$infos['type'] = isset($infos['type']) ? $infos['type'] : '';
+			switch($infos['type']){
+				case 'list':
+					$stream[] = array('Macros disponibles :' => $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').' (liste)'.PHP_EOL);
+					$stream[] = array('Macros disponibles :' => $indentation.'{{#'.$macro.'}}'.PHP_EOL);
+					if(is_array($infos['value']) && isset($infos['value'][0])) $stream = array_merge($stream, self::recursive_sample($infos['value'][0],$level+1));
+					$stream[] = array('Macros disponibles :' =>$indentation.'{{/'.$macro.'}}');
+				break;
+				case 'object':
+					$stream[] = array('Macros disponibles :' => $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').PHP_EOL);
+					$stream = array_merge($stream, self::recursive_sample($infos['value'],$level+1,$parent.$macro));
+				break;
+				case 'image' : 
+					$stream[] = array('Macros disponibles :' => $indentation.'{{'.$parent.$macro.'::image}} : '.( !isset($infos['label']) ? '': $infos['label']).PHP_EOL);
+				break;
+				default : 
+					$stream[] = array('Macros disponibles :' => $indentation.'{{'.$parent.$macro.'}} : '.( !isset($infos['label']) ? '': $infos['label']).PHP_EOL);
+				break;
+			}
+		}
+		return $stream;
+	}
+	
 	//Remplacement du tag d'image par l'image concernée
 	//dans le fichier modèle
-	public static function add_image($worksheet, $macro, $value, $cellCoord, $scale=2){
-		$value = substr($value,2);
+	public function add_image($worksheet, $macro, $value, $cellCoord, $scale=2){
+		$finfo = new finfo(FILEINFO_MIME);
+		$mime = $finfo->buffer($value);
+		
+		$ext = 'jpg';
+		switch($mime){
+			case 'image/jpeg': $ext = 'jpeg'; break;
+			case 'image/png': $ext = 'png'; break;
+			case 'image/gif': $ext = 'gif'; break;
+		}
+		if($mime == 'image/jpg') $mime = 'image/jpeg';
+
+		$path = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.'.$ext;
+		file_put_contents($path, $value);
+		$this->tempFiles[] = $path;
+
 		//On supprime le tag
 		$cellVal = str_replace('{{'.$macro.'}}', '', $worksheet->getCell($cellCoord)->getValue());
 		$worksheet->getCell($cellCoord)->setValue($cellVal);
@@ -31,7 +72,7 @@ class ExcelExport
 		$rowIndex = $cellMatches[1];
 
 		//On récupère les infos de l'image
-		list($width, $height) = getimagesize($value);
+		list($width, $height) = getimagesize($path);
 
 		//On définit la taille de la cellule à celle de l'image
 		$cellWidth = $worksheet->getColumnDimension($cellIndex);
@@ -44,101 +85,81 @@ class ExcelExport
 		//On ajoute l'image dans la feuille
 		$drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
 		$drawing->setName($macro);
-		$drawing->setPath($value);
+		$drawing->setPath($path);
 		$drawing->setResizeProportional(true);
 		$drawing->setWidthAndHeight($width/$scale,$height/$scale);
 		$drawing->setCoordinates($cellCoord);
 		$drawing->setWorksheet($worksheet);
 	}
 
-	//Remplace les données d'un jeu de données en fonction
-	//des tags rencontrés et fournis par le jeu de données
-	public static function replace_data($worksheet, $data=array(), $cellCoord, $cellContent, $imgScale=2){
-		//Pour chaque élément de l'entité
-		foreach ($data as $tag => $value) {
-			if(strpos($cellContent, '{{'.$tag.'}}') !== false){
-				$cellVal = $worksheet->getCell($cellCoord)->getValue();
-				//Ajout des images
-				if(substr($value,0,2)=='::'){
-					ExcelExport::add_image($worksheet, $tag, $value, $cellCoord, $imgScale);
-					continue;
-				}
-				if(is_numeric($value) || preg_match("/\d+(?:\.\d{1,2})? [€,$,£,₽,¥]/",$value))
-					$worksheet->getStyle($cellCoord)->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER);
-
-				$cellVal = str_replace('{{'.$tag.'}}', $value, $cellVal);
-				$worksheet->getCell($cellCoord)->setValue($cellVal);
-			}
-		}
+	public function start($stream){
+		require(LIB_PATH.'PhpSpreadsheet'.SLASH.'vendor'.SLASH.'autoload.php');
+		$this->source = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.xlsx';
+		file_put_contents($this->source,utf8_decode($stream));
+
+		$this->spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($this->source);
+		$this->spreadsheet->getActiveSheet()->getPageMargins()->setTop(0.2);
+		$this->spreadsheet->getActiveSheet()->getPageMargins()->setRight(0.2);
+		$this->spreadsheet->getActiveSheet()->getPageMargins()->setLeft(0.2);
+		$this->spreadsheet->getActiveSheet()->getPageMargins()->setBottom(0.2);
+		$this->spreadsheet->getActiveSheet()->getPageMargins()->setFooter(0.5);
+		$this->spreadsheet->getActiveSheet()->getPageMargins()->setHeader(0.5);
+		$this->spreadsheet->getActiveSheet()->getPageSetup()->setHorizontalCentered(true);
+		
 	}
-
 	//Récupère et gère la structure du remplacement
 	//des données dans le fichier template fourni
-	public static function from_template($source, $data, $return){
+	public function from_template($stream, $data){
 		require(LIB_PATH.'PhpSpreadsheet'.SLASH.'vendor'.SLASH.'autoload.php');
 
-		$destination = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.xlsx';
-		$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load(File::dir().$source);
-		$spreadsheet->getActiveSheet()->getPageMargins()->setTop(0.2);
-		$spreadsheet->getActiveSheet()->getPageMargins()->setRight(0.2);
-		$spreadsheet->getActiveSheet()->getPageMargins()->setLeft(0.2);
-		$spreadsheet->getActiveSheet()->getPageMargins()->setBottom(0.2);
-		$spreadsheet->getActiveSheet()->getPageMargins()->setFooter(0.5);
-		$spreadsheet->getActiveSheet()->getPageMargins()->setHeader(0.5);
-		$spreadsheet->getActiveSheet()->getPageSetup()->setHorizontalCentered(true);
-
 		//Pour chaque feuille dans le classeur Excel
-		foreach ($spreadsheet->getAllSheets() as $wrkSheetIdx => $worksheet) {
+		foreach ($this->spreadsheet->getAllSheets() as $wrkSheetIdx => $worksheet) {
+
 			//On récupère la zone de travail (pour ne pas se perdre dans des cellules vide)
 			//Avec la colonne max et la ligne max
 			$maxCol = 'A';
 			$maxRow = 0;
 			foreach ($worksheet->getCoordinates() as $coord) {
-				preg_match_all('~[A-Z]+|\d+~', $coord, $matches);
+				preg_match_all("/[A-Z]+|\d+/", $coord, $matches);
 				$matches = reset($matches);
 				$currCol = PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($matches[0]);
 				$currRow = $matches[1];
+				$currValue = $worksheet->getCell($coord)->getValue();
 
-				if($maxCol < $currCol)
-					$maxCol = $currCol;
-
-				if($maxRow < $currRow)
-					$maxRow = $currRow;
+				if($maxCol < $currCol && !empty($currValue)) $maxCol = $currCol;
+				if($maxRow < $currRow && !empty($currValue)) $maxRow = $currRow;
 			}
 
-			//On parcours une fois le contenu du la feuille
+			//On parcourt une fois le contenu du la feuille
 			//pour avoir les différents bords des boucles, si
 			//il y en a dans le fichier.
 			$rows = $worksheet->toArray('', true, true, true);
-			$loopDatas = array('startLine' => 0,'endLine' => 0);
+			$loopDatas = array('startLine'=>0, 'endLine'=>0);
 			$finalValues = array();
-			$loopStart = $loopEnd = false;
+
 			foreach ($rows as $rowIdx => $cell) {
+				if($rowIdx>$maxRow) continue;
+
 				foreach ($cell as $cellIdx => $content) {
 					if(empty($content) && PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol) continue;
 
-					foreach ($data as $macro => $value) {
-						if(!is_array($value)) continue;
-						$entityCount = count($value);
-
-						if(strpos($content, '{{/'.$macro.'}}') !== false) {
-							$loopDatas['endLine'] = $rowIdx;
-							$loopDatas['endColumn'] = $cellIdx;
-							$loopDatas['endCoord'] = $cellIdx.$rowIdx;
-							$loopDatas['totalRow'] = $loopDatas['endLine'] - $loopDatas['startLine'];
-							$loopDatas['totalCol'] = letters_to_numbers($loopDatas['endColumn']) - letters_to_numbers($loopDatas['startColumn']);
-							$loopEnd = true;
-						}
-
-						if(!$loopEnd && strpos($content, '{{#'.$macro.'}}') !== false) {
-							$loopDatas['startLine'] = $rowIdx;
-							$loopDatas['startColumn'] = $cellIdx;
-							$loopDatas['startCoord'] = $cellIdx.$rowIdx;
-							$loopStart = true;
-						}
+					//@TODO: Faire en récursif !
+					if(preg_match("/\{\{\#([^\/\#}]*)\}\}/is", $content, $matches)){
+						$loopDatas['key'] = $matches[1];
+						$loopDatas['startLine'] = $rowIdx;
+						$loopDatas['startColumn'] = $cellIdx;
+						$loopDatas['startCoord'] = $cellIdx.$rowIdx;
+					}
+					if(preg_match("/\{\{\/([^\/\#}]*)\}\}/is", $content, $matches)){
+						$loopDatas['endLine'] = $rowIdx;
+						$loopDatas['endColumn'] = $cellIdx;
+						$loopDatas['endCoord'] = $cellIdx.$rowIdx;
+						$loopDatas['totalRow'] = $loopDatas['endLine'] - $loopDatas['startLine'];
+						$loopDatas['totalCol'] = letters_to_numbers($loopDatas['endColumn']) - letters_to_numbers($loopDatas['startColumn']);
 					}
 				}
 			}
+
 			//Définition type de boucle
 			if($loopDatas['startLine'] != 0 && $loopDatas['endLine'] != 0)
 				$loopDatas['loopType'] = ($loopDatas['startLine'] != $loopDatas['endLine']) ? 'vertical' : 'horizontal';
@@ -148,9 +169,10 @@ class ExcelExport
 				if($loopDatas['loopType'] == 'vertical'){
 					//On parcourt par ligne puis par colonne
 					foreach ($rows as $rowIdx => $cell) {
+						if($rowIdx>$maxRow) continue;
 						foreach ($cell as $cellIdx => $content) {
-							if($rowIdx > $loopDatas['startLine'] && $rowIdx < $loopDatas['endLine'])
-								$finalValues[$rowIdx][$cellIdx][] = $content;
+							if(PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol || ($rowIdx <= $loopDatas['startLine'] || $rowIdx >= $loopDatas['endLine'])) continue;
+							$finalValues[$rowIdx][$cellIdx][] = $content;
 						}
 					}
 				} else {
@@ -158,8 +180,7 @@ class ExcelExport
 					for($col = $loopDatas['startColumn']; $col <= $loopDatas['endColumn']; ++$col){
 						if($col == $loopDatas['startColumn'] || $col == $loopDatas['endColumn']) continue;
 						for($row = $loopDatas['startLine']; $row <= $maxRow; ++$row) {
-						    $content = $worksheet->getCell($col.$row)->getValue();
-							$finalValues[$col][$row][] = $content;
+							$finalValues[$col][$row][] = $worksheet->getCell($col.$row)->getValue();
 						}
 					}
 				}
@@ -168,64 +189,64 @@ class ExcelExport
 			//On remplace les données à l'intérieur
 			//des boucles si des boucles sont présentes
 			if(isset($finalValues) && !empty($finalValues)){
-
-				//Pour chaque value du jeu de données
-				foreach ($data as $macro => $value) {
-					if(!is_array($value)) continue;
-					$worksheet->getCell($loopDatas['startCoord'])->setValue('');
-					$worksheet->getCell($loopDatas['endCoord'])->setValue('');
-
-					//Pour chaque entité
-					foreach ($value as $i => $entity) {
-						$rowIterator = $colIterator = 0;
-						if($loopDatas['loopType'] == 'vertical'){
-							unset($finalValues[$loopDatas['endLine']]);
-
-							//Pour chaque ligne
-							foreach ($finalValues as $rIdx => $cell) {
-								$lineIdx = ($loopDatas['startLine']+1)+(($loopDatas['totalRow']-1)*$i)+$rowIterator;
-								//On ajoute 1 car on insère AVANT la ligne, et non APRÈS
-								$rowWhereInsert = $i==0 ? $loopDatas['endLine']+1 : $rowWhereInsert += 1;
-								$worksheet->insertNewRowBefore($rowWhereInsert, 1);
-
-								//Pour chaque cellule
-								foreach ($cell as $cIdx => $content) {
-									$currCoord = $cIdx.$lineIdx;
-									$lineRef = $lineIdx-($loopDatas['totalRow']-1);
-									//Dans le cas où $i vaut 0, cas particulier, on remplace directement les données dans la feuille
-									$referentCell = $i==0 ? $currCoord : $cIdx.$lineRef;
-
-									$worksheet->setCellValue($currCoord, $content[0]);
-									$worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord);
-
-									//On remplace les données
-									ExcelExport::replace_data($worksheet, $entity, $currCoord, $content[0], 4);
-								}
-								$rowIterator++;
+				$values = $this->decomposeKey($data, $loopDatas['key']);
+
+				//Pour chaque entité
+				foreach ($values as $i => $entity) {
+					$rowIterator = $colIterator = 0;
+					
+					if($loopDatas['loopType'] == 'vertical'){
+						unset($finalValues[$loopDatas['endLine']]);
+
+						//On ajoute 1 car on insère AVANT la ligne, et non APRÈS
+						$rowWhereInsert = $i==0 ? $loopDatas['endLine']+1 : $rowWhereInsert += 1;
+						$worksheet->insertNewRowBefore($rowWhereInsert, 1);
+
+						//Pour chaque ligne
+						foreach ($finalValues as $rIdx => $cell) {
+							$lineIdx = ($loopDatas['startLine']+1) + (($loopDatas['totalRow']-1)*$i) + $rowIterator;
+							$maxRow += 1;
+
+							//Pour chaque cellule
+							foreach ($cell as $cIdx => $content) {
+								$currCoord = $cIdx.$lineIdx;
+								$lineRef = $lineIdx-($loopDatas['totalRow']-1);
+								//Dans le cas particulier où $i vaut 0, on remplace directement les données dans la feuille
+								$referentCell = $i==0 ? $currCoord : $cIdx.$lineRef;
+
+								$worksheet->setCellValue($currCoord, $content[0]);
+								$worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord);
+								$maxCol += 1;
+
+								//On remplace les données
+								ExcelExport::replace_data($worksheet, $entity, $currCoord, 4);
 							}
-						} else {
-							//Pour chaque colonne
-							foreach ($finalValues as $cIdx => $col) {
-								$colIdx = numbers_to_letters((letters_to_numbers($loopDatas['startColumn'])+1)+(($loopDatas['totalCol']-1)*$i)+$colIterator);
-
-								//On ajoute 1 car on insère AVANT la ligne, et non APRÈS
-								$colWhereInsert = $i==0 ? numbers_to_letters(letters_to_numbers($loopDatas['endColumn'])+1) : numbers_to_letters(letters_to_numbers($colWhereInsert)+1);
-								$worksheet->insertNewColumnBefore($colWhereInsert, 1);
-
-								//Pour chaque cellule
-								foreach ($col as $rIdx => $content) {
-									$currCoord = $colIdx.$rIdx;
-									//Dans le cas où $i vaut 0, cas particulier, on remplace directement les données dans la feuille
-									$referentCell = $i==0 ? $currCoord : $cIdx.$rIdx;
-									
-									$worksheet->setCellValue($currCoord, $content[0]);
-									$worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord);
-
-									//On remplace les données
-									ExcelExport::replace_data($worksheet, $entity, $currCoord, $content[0], 4);
-								}
-								$colIterator++;
+							$rowIterator++;
+						}
+					} else {
+						//On ajoute 1 car on insère AVANT la ligne, et non APRÈS
+						$colWhereInsert = $i==0 ? numbers_to_letters(letters_to_numbers($loopDatas['endColumn'])+1) : numbers_to_letters(letters_to_numbers($colWhereInsert)+1);
+						$worksheet->insertNewColumnBefore($colWhereInsert, 1);
+
+						//Pour chaque colonne
+						foreach ($finalValues as $cIdx => $col) {
+							$colIdx = numbers_to_letters((letters_to_numbers($loopDatas['startColumn'])+1)+(($loopDatas['totalCol']-1)*$i)+$colIterator);
+							$maxCol += 1;
+							
+							//Pour chaque cellule
+							foreach ($col as $rIdx => $content) {
+								$currCoord = $colIdx.$rIdx;
+								//Dans le cas où $i vaut 0, cas particulier, on remplace directement les données dans la feuille
+								$referentCell = $i==0 ? $currCoord : $cIdx.$rIdx;
+								
+								$worksheet->setCellValue($currCoord, $content[0]);
+								$worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord);
+								$maxRow += 1;
+								
+								//On remplace les données
+								ExcelExport::replace_data($worksheet, $entity, $currCoord, 4);
 							}
+							$colIterator++;
 						}
 					}
 				}
@@ -233,45 +254,114 @@ class ExcelExport
 
 			//On remplace le reste des tag présents
 			//sur la feuille de calcul du fichier template
-			foreach ($worksheet->getRowIterator() as $row) {
-				$cellIterator = $row->getCellIterator();
-
-				foreach ($cellIterator as $cell) {
-					$cellVal = $cell->getValue();
-					$cellIndex = $cell->getColumn();
-					if(empty($cellVal) && PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIndex)>$maxCol) continue;
-
-					foreach ($data as $macro => $value) {
-						if(is_array($value)) continue;
-
-						//Ajout des valeurs
-						if(strpos($cellVal, '{{'.$macro.'}}') !== false){
-							//Ajout des images
-							if(substr($value,0,2)=='::'){
-								ExcelExport::add_image($worksheet, $macro, $value, $cell->getCoordinate(), 2);
-								continue;
-							}
-							
-							if(is_numeric($value)) 
-								$worksheet->getStyle($cell->getCoordinate())->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER_00); 
-							$cellVal = str_replace('{{'.$macro.'}}', $value, $cellVal);
-							$cell->setValue($cellVal);
-						}
-					}
+			foreach ($worksheet->getRowIterator() as $rowIdx => $row) {
+				if($rowIdx>$maxRow) continue;
+
+				foreach ($row->getCellIterator() as $cellIdx => $cell) {
+					if(empty($cell->getValue()) && PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol) continue;
+					$this->replace_data($worksheet, $data, $cellIdx.$rowIdx);
+				}
+			}
+
+			//@TODO: Gérer plusieurs boucles sur le document, et décrémenter pour chaque boucle présente les début et fin par suppression de lignes/colonnes
+			//(cf. fin de fichier)
+			//En fct du type de boucle on supprime les lignes/colonnes de début et fin de boucle
+			if(isset($loopDatas['loopType'])){
+				if($loopDatas['loopType'] == 'vertical'){
+					//On supprime la ligne de début de boucle (la ligne avec le tag de début de loop)
+					$worksheet->removeRow($loopDatas['startLine']);
+
+					//Si le nb d'itération sur les lignes est supérieur au nb de lignes de données à remplacer pour 1 itération
+					//Alors on indique à -1 sinon -2
+					$rowToDelete = $rowIterator > $loopDatas['endLine']-$loopDatas['startLine']-1 ? $rowWhereInsert-1 : $rowWhereInsert-2;
+					//On supprime la ligne de fin de boucle (la ligne avec le tag de fin de loop)
+					$worksheet->removeRow($rowToDelete);
+
+					//On supprime les N lignes insérées à chaque itération de boucle
+					for ($i=1; $i<=$loopDatas['endLine']-$loopDatas['startLine']-1; $i++)
+						$worksheet->removeRow($rowToDelete+$i);
+
+				} else if($loopDatas['loopType'] == 'horizontal'){
+					// On supprime la colonne de début de boucle
+					$worksheet->removeColumn($loopDatas['startColumn']);
+					
+					//Si le nb d'itération sur les colonnes est supérieur au nb de colonnes de données à remplacer pour 1 itération
+					//Alors on indique à -1 sinon -2
+					$colToDelete = $colIterator > letters_to_numbers($loopDatas['endColumn'])-letters_to_numbers($loopDatas['startColumn'])-1 ? letters_to_numbers($colWhereInsert)-1 : letters_to_numbers($colWhereInsert)-2;
+					//On supprime la colonne de fin de boucle (la colonne avec le tag de fin de loop)
+					$worksheet->removeColumn(numbers_to_letters($colToDelete));
+
+					for ($i=1; $i<=$loopDatas['endColumn']-$loopDatas['startColumn']-1; $i++)
+						$worksheet->removeColumn(numbers_to_letters($colToDelete+$i));
 				}
 			}
 		}
+	}
 
-		$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
-		$writer->save($destination);
+	//Remplace les données d'un jeu de données en fonction
+	//des tags rencontrés et fournis par le jeu de données
+	public function replace_data($worksheet, $data=array(), $cellCoord, $imgScale=2){
+		$cell = $worksheet->getCell($cellCoord);
+		$cellVal = $cell->getValue();
+
+		//gestion des simples variables
+		$cellVal = preg_replace_callback('/{{([^#\/}]*)}}/',function($matches) use ($data,$cellCoord,$imgScale,$worksheet) {
+			$key = $matches[1];
+			$keyInfos = explode('::',$key);
+			$key = $keyInfos[0];
+			$type = isset($keyInfos[1]) ? $keyInfos[1] : 'string';
+
+			$value = $this->decomposeKey($data,$key);
+			if($type =='image'){
+				if(isset($value)) ExcelExport::add_image($worksheet, $key, $value, $cellCoord, $imgScale);
+				return;
+			}
+
+			if(is_numeric($value))
+				$worksheet->getStyle($cellCoord)->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER);
+			else if(preg_match("/\d+(?:\.\d{1,2})? €/",$value))
+				$worksheet->getStyle($cellCoord)->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE);
 
-		if($return!='stream') return $destination;
+			if(!isset($value)) return $this->formatValue($type,$matches[0]);
+			if(is_array($value)) return 'Array';
+			return $this->formatValue($type,$value);
+		},$cellVal); 
 
-		$stream = file_get_contents($destination);
-		unlink($destination);
+		$cell->setValue($cellVal);
+	}
+
+	public function end($stream,$data){
+		$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($this->spreadsheet, 'Xlsx');
+		$writer->save($this->source);
+		$stream = file_get_contents($this->source);
+		unlink($this->source);
+		
+		//suppression des images temporaires
+		foreach ($this->tempFiles as $file) {
+			if(file_exists($file)) unlink($file);
+		}
 		return $stream;
 	}
 
+	public function formatValue($type,$value){
+		return $value;
+	}
+
+	public function decomposeKey($datas,$key){
+		if(array_key_exists($key, $datas)) return isset($datas[$key]) ? $datas[$key] : '' ;
+		
+		$attributes = explode('.',$key);
+		$current = $datas;
+		$value = null;
+
+		foreach ($attributes as $attribute) {
+			if(!array_key_exists($attribute, $current))  break;
+			$current = $current[$attribute];
+			$value = isset($current) ?  $current :  '';
+		}
+		return $value;
+	}
+
 	//Copie l'intégralité de la ligne depuis des 
 	//positions données à l'endroit voulu
 	public static function copy_full_row(&$ws_from, &$ws_to, $row_from, $row_to) {
@@ -285,5 +375,26 @@ class ExcelExport
 			$cell_to->setValue($cell_from->getValue());
 		}
 	}
+
+	//Gère la suppression des lignes/colonnes et la ré-adaptation des indexs de bornes de boucles 
+	// public static function removeShiftRow($worksheet, $loops, $idx, $nb){
+	// 	//$i vaut 2i puis 2i+1
+	// 	foreach ($loops as $i => $loop) {
+	// 		if($loop['loopType'] == 'vertical'){
+	// 			$worksheet->removeRow($loop['startLine'] - (2*$i));
+	// 			$worksheet->removeRow($loop['endLine'] - ((2*$i)+1));
+	// 			
+	// 			$loops[$i]['startLine'] = $loop['startLine'] - (2*$i)
+	// 			
+	// 	//@TODO: Gérer la ré-assignation des coordonnées
+		// startCoord
+		// endCoord
+
+	// 		} else if($loop['loopType'] == 'horizontal'){
+	// 			$worksheet->removeColumn(chr(ord($loop['startColumn']) - (2*$i));
+	// 			$worksheet->removeColumn(chr(ord($loop['endColumn']) - ((2*$i)+1));
+	// 		}
+	// 	}
+	// }
 }
 

+ 32 - 0
plugin/export/template/FillPdfExport.class.php

@@ -0,0 +1,32 @@
+<?php
+
+class FillPdfExport{
+	public $mime = 'application/pdf';
+	public $extension = 'pdf';
+	public $description = 'Fichier informations formulaire PDF';
+
+	public  function sample($dataset,$level = 0){
+		return 'Aucun exemple disponible';
+	}
+
+	public function start($stream,&$datas){
+		$datas = $this->encodeValue($datas);
+	}
+
+	public function encodeValue($datas){
+		foreach ($datas as $key=>$data) {
+			$datas[$key] = is_array($data) ? $this->encodeValue($data) : utf8_decode($data);
+		}
+		return $datas;
+	}
+
+	public function from_template($stream,$datas){
+		$temp = str_replace('\\\\','\\',File::temp().rand(0,1000000));
+		file_put_contents($temp, $stream );
+		$stream = Pdf::fillData($temp,$datas);
+		unlink($temp);
+		return $stream;
+
+	}
+}
+?>

+ 37 - 13
plugin/export/template/HtmlExport.class.php

@@ -1,24 +1,48 @@
 <?php
 require_once(__DIR__.SLASH.'TextExport.class.php');
 class HtmlExport extends TextExport{
-	public static $mime = 'text/html';
-	public static $extension = 'html';
-	public static $description = 'Fichier page web HTML';
+	public  $mime = 'text/html';
+	public  $extension = 'html';
+	public  $description = 'Fichier page web HTML';
 
-	public static function sample($dataset){
+	public  function sample($dataset,$level = 0,$parent=''){
 		$stream = '<!DOCTYPE html><html lang="fr"><head><meta charset="utf-8"></head><body>';
-		foreach($dataset as $macro => $infos)
-			$stream .= ($infos['type']=='list') ? '{{#'.$macro.'}}{{/'.$macro.'}} : '.$infos['desc'].'<br>' : '{{'.$macro.'}} : '.$infos['desc'].'<br>'.PHP_EOL;
+		$stream .= self::recursive_sample($dataset, $level,$parent);
 		$stream .= '</body></html>';
 		return $stream;
 	}
-
-	public static function from_template($source, $data, $return, $isHtml=true){
-		$htmlRaw = TextExport::from_template($source, $data, $return, $isHtml);
-		$stream = $return=='stream' ? $htmlRaw : file_get_contents($htmlRaw);
-
-	    if($return!='stream') return $htmlRaw;
-	    return $stream;
+	public function recursive_sample($dataset,$level = 0,$parent=''){
+		$stream = '';
+		$parent = ($parent!=''?$parent.'.':'');
+		$indentation = str_repeat("<div style='width:20px;display:inline-block;height:10px;'></div>", $level);
+		foreach($dataset as $macro => $infos){
+			$infos['type'] = isset($infos['type']) ? $infos['type'] : '';
+			
+			switch($infos['type']){
+				case 'list':
+					$stream .= $indentation.'<h3>-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').' (liste)'.'</h3>'.PHP_EOL;
+					$stream .= $indentation.'{{#'.$parent.$macro.'}}'.'<br/>'.PHP_EOL;;
+					if(is_array($infos['value']) && isset($infos['value'][0])) $stream .=  self::sample($infos['value'][0],$level+1);
+					$stream .=$indentation.'<br>{{/'.$parent.$macro.'}}';
+				break;
+				case 'image':
+					$stream .= $indentation.'<img src="{{'.$parent.$macro.'::image}}">'.$infos['label'];
+				break;
+				case 'object':
+					$stream .= '<h3>'.$indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').'</h3>'.PHP_EOL;;
+					$stream .= self::sample($infos['value'],$level+1,$parent.$macro);
+				break;
+				default : 
+					$stream .= $indentation.'{{'.$parent.$macro.'}} : '.( !isset($infos['label']) ? '': $infos['label']).'<br/>'.PHP_EOL;
+				break;
+			}
+		}
+		return $stream;
+	}
+	
+	public  function formatValue($type,$value){
+		if($type == 'image') return 'data:image/png;base64,'.base64_encode($value);
+		return $value;
 	}
 }
 ?>

+ 11 - 23
plugin/export/template/PdfExport.class.php

@@ -1,36 +1,24 @@
 <?php
 require_once(__DIR__.SLASH.'HtmlExport.class.php');
 class PdfExport extends HtmlExport{
-	public static $mime = 'application/pdf';
-	public static $extension = 'pdf';
-	public static $description = 'Fichier informations PDF';
+	public $mime = 'application/pdf';
+	public $extension = 'pdf';
+	public $description = 'Fichier informations PDF';
 
-	public static function sample($dataset){
+	public function sample($dataset, $level=0, $parent=''){
 		$stream = parent::sample($dataset);
-
 		$pdf = new Pdf($stream);
 		$stream = $pdf->generate();
-
 		return $stream;
 	}
 
-	public static function from_template($source, $data, $return, $isHtml=true){
-		$htmlRaw = HtmlExport::from_template($source, $data, $return, $isHtml);
-		$stream = $return=='stream' ? $htmlRaw : file_get_contents($htmlRaw);
-
-	    $pdf = new Pdf($stream);
-	    $pdfStream = $pdf->generate();
-
-	    $destination = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.pdf';
-	    file_put_contents($destination, $pdfStream);
-	    if($return!='stream') {
-	    	unlink($htmlRaw);
-	    	return $destination;
-	    }
-	    
-	    $stream = file_get_contents($destination);
-		unlink($destination);
-	    return $stream;
+	public function end($stream, $datas, $options){
+		$margin = isset($options['options']['margin']) ? $options['options']['margin'] : array();
+		$format = isset($options['options']['format']) ? $options['options']['format'] : 'A4';
+		$orientation = isset($options['options']['orientation']) ? $options['options']['orientation'] : 'Portrait';
+	    $pdf = new Pdf($stream,$margin,$format,$orientation);
+		
+		return $pdf->generate();
 	}
 }
 ?>

+ 102 - 74
plugin/export/template/TextExport.class.php

@@ -1,95 +1,123 @@
 <?php 
 
-class TextExport
-{
-	public static $mime = 'text/plain';
-	public static $extension = 'txt';
-	public static $description = 'Fichier de texte brut';
+class TextExport {
+	public $mime = 'text/plain';
+	public $extension = 'txt';
+	public $description = 'Fichier de texte brut';
 
-	public static function sample($dataset){
+	public function sample($dataset,$level = 0,$parent=''){
 		$stream = '';
-		foreach($dataset as $macro => $infos)
-			$stream .= ($infos['type']=='list') ? '{{#'.$macro.'}}{{/'.$macro.'}} : '.$infos['desc'].PHP_EOL : '{{'.$macro.'}} : '.$infos['desc'].PHP_EOL;
+		$parent = ($parent!=''?$parent.'.':'');
+		$indentation = str_repeat("\t", $level);
+		foreach($dataset as $macro => $infos){
+			$infos['type'] = isset($infos['type']) ? $infos['type'] : '';
+			switch($infos['type']){
+				case 'list':
+					$stream .= $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').' (liste)'.PHP_EOL;
+					$stream .= $indentation.'{{#'.$parent.$macro.'}}'.PHP_EOL;
+					if(is_array($infos['value']) && isset($infos['value'][0])) $stream .=  self::sample($infos['value'][0],$level+1);
+					$stream .=$indentation.'{{/'.$parent.$macro.'}}';
+				break;
+				case 'object':
+					$stream .= $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').PHP_EOL;
+					$stream .= self::sample($infos['value'],$level+1,$parent.$macro);
+				break;
+				case 'image': break;
+				default : 
+					$stream .= $indentation.'{{'.$parent.$macro.'}} : '.( !isset($infos['label']) ? '': $infos['label']).PHP_EOL;
+				break;
+			}
+		}
 		return $stream;
 	}
 
-	public static function from_template($source, $data, $return, $isHtml=false){
-		$fileParts = explode('.', $source);
-		$ext = count($fileParts)>1 ? '.'.end($fileParts) : '';
-		if(count(explode(SLASH, $ext)) > 1) $ext = '';
+	//permet aux exports enfant de surcharger la méthode pour convertir des valeurs en fct de leurs type sur un export particulier
+	// (ex: convertion des flux images en base64 sur les fichiers html)
 
-		$destination = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).$ext;
-		$source = File::dir().$source;
-		copy($source,$destination);
+	public  function formatValue($type,$value){
+		if($type == 'image') return '';
+		return $value;
+	}
 
-		$stream = file_get_contents($destination);
-		if(mb_detect_encoding($stream, 'UTF-8', true) == false) $stream = utf8_encode($stream);
+	public function decomposeKey($datas,$key){
+		if(array_key_exists($key, $datas)) return isset($datas[$key]) ? $datas[$key] : '';
 		
-		foreach($data as $tag=>$value){
+		$attributes = explode('.',$key);
+		$current = $datas;
+		$value = null;
+
+        foreach ($attributes as $attribute) {
+        	if(!array_key_exists($attribute, $current)) break;
+        	$current = $current[$attribute];
+        	$value = isset($current) ? $current : '';
+        }
+        return $value;
+	}
+
+	public function from_template($stream, $datas){
+		//if / loop
+		$loopIfRegex = '/\{\{\#([^\/\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
+		$stream =  preg_replace_callback($loopIfRegex,function($matches) use ($datas) {
+			$key = $matches[1];
+			$streamTpl = $matches[2];
+			$keyInfos = explode('::',$key);
+			$key = $keyInfos[0];
+			$type = isset($keyInfos[1]) ? $keyInfos[1] : 'string';
+
+			$value = $this->decomposeKey($datas,$key);
+			
+			//gestion des boucles
 			if(is_array($value)){
-				$stream =  preg_replace_callback(
-					'#\{\{\#'.$tag.'\}\}(.*)\{{\/'.$tag.'\}\}#si',
-					function ($match) use($value) {
-						$html = '';
-						foreach($value as $line){
-							$bloc = $match[1];
-							foreach($line as $k=>$v){
-								$bloc = str_replace('{{'.$k.'}}',$v,$bloc);
-								$bloc = self::if_condition($bloc, $line);
-								$bloc = self::else_condition($bloc, $line);
-							}
-							$html .=$bloc;
-						}
-						return $html;
-					},
-					$stream
-				);
-				continue;
+				$stream = '';
+				foreach($value as $line){
+					$localData = is_array($line) ? array_merge($datas,$line) : $datas;
+					$stream .= $this->from_template($streamTpl,$localData);
+				}
+				return $stream;
+			//gestion des if
+			}else{
+				return !isset($value) || (!is_array($value) && empty($value)) || (is_array($value) && count($value)==0) ? '' : $this->from_template($streamTpl,$datas);
 			}
+		},$stream);
 
-			//Pas d'images possibles dans un fichier texte
-			if(substr($value,0,2)=='::' && !$isHtml) continue;
+		//gestion des else
+		$elseRegex = '/\{\{\^([^\/\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
+		$stream =  preg_replace_callback($elseRegex,function($matches) use ($datas) {
+			$key = $matches[1];
+			$streamTpl = $matches[2];
 
-			//Remplacement d'image pour les fichiers HTML/PDF
-			if(substr($value,0,2)=='::') $value = substr($value,2);
-			$stream = str_replace('{{'.$tag.'}}', $value, $stream);
-		}
+			$keyInfos = explode('::',$key);
+			$key = $keyInfos[0];
+			$type = isset($keyInfos[1]) ? $keyInfos[1] : 'string';
 
-		$stream = self::if_condition($stream, $data);
-		$stream = self::else_condition($stream, $data);
+			$value = $this->decomposeKey($datas,$key);
 
-		file_put_contents($destination, $stream);
-		if($return!='stream') return $destination;
+			if(is_array($value)){
+				$stream = '';
+				foreach($value as $line){
+					$localData = is_array($line) ? array_merge($datas,$line) : $datas;
+					$stream .= $this->from_template($streamTpl,$localData);
+				}
+				return $stream;
+			//gestion des else
+			}else{
+				return !isset($value) || (!is_array($value) && empty($value)) || (is_array($value) && count($value)==0) ? $this->from_template($streamTpl,$datas) : '';
+			}
+		},$stream);
 
-		$stream = file_get_contents($destination);
-		unlink($destination);
-		return ($isHtml ? $stream : utf8_decode($stream));
-	}
+		//gestion des simples variables
+	    $stream = preg_replace_callback('/{{([^#\/}]*)}}/',function($matches) use ($datas) {
+	        $key = $matches[1];
 
-	public static function if_condition($stream, $data){
-		// $ifRegex = '/\{\{\#([^\/\:\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
-		// $stream = recursive_preg_replace($ifRegex, $stream, $data);
-		
-		//conditions
-		$ifRegex = '/\{\{\#([^\/\:\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
-		return preg_replace_callback($ifRegex,function($matches) use ($data) {
-			$key = $matches[1];
-			$stream = $matches[2];
-			return !isset($data[$key]) || (!is_array($data[$key]) && empty($data[$key])) || (is_array($data[$key]) && count($data[$key])==0) ?'':$stream;
-		},$stream);
-	}
+	        $keyInfos = explode('::',$key);
+			$key = $keyInfos[0];
+			$type = isset($keyInfos[1]) ? $keyInfos[1] : 'string';
 
-	public static function else_condition($stream, $data){
-		// $elseRegex = '/\{\{\^([^\/\:\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
-		// $stream = recursive_preg_replace($elseRegex, $stream, $data);
-		
-		//conditions
-		$elseRegex = '/\{\{\^([^\/\:\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
-		return preg_replace_callback($elseRegex,function($matches) use ($data) {
-			$key = $matches[1];
-			$stream = $matches[2];
-			return (isset($data[$key]) && !is_array($data[$key]) && !empty($data[$key])) || (is_array($data[$key]) && count($data[$key])>0) ?'':$stream;
-		},$stream);
+	        $value = $this->decomposeKey($datas,$key);
+	        if(!isset($value)) return $this->formatValue($type,$matches[0]);
+	        if(is_array($value)) return 'Array';
+	        return $this->formatValue($type,$value);
+	    },$stream); 
+		return $stream;
 	}
 }
-

+ 178 - 113
plugin/export/template/WordExport.class.php

@@ -5,15 +5,15 @@
  * @category Plugin
  * @license copyright
  */
-class WordExport
-{
-	public static $mime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
-	public static $extension = 'docx';
-	public static $description = 'Fichier de traitement de texte Word';
-
+class WordExport {
+	public $mime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
+	public $extension = 'docx';
+	public $description = 'Fichier de traitement de texte Word';
+	public $destination;
 	public $zip,$body,$header,$footer,$relationships;
 
-	public static function sample($dataset){
+	public  function sample($dataset,$level = 0,$parent=''){
+		$rows = array();
 		$rows[] = array(
 			'content' => 'Macros disponibles :',
 			'style' => array(
@@ -21,21 +21,58 @@ class WordExport
 				'font-size' => 16
 			)
 		);
-
-		foreach($dataset as $macro => $infos) {
-			$rows[] = array(
-				'content' => ($infos['type']=='list') ? '{{#'.$macro.'}}{{/'.$macro.'}} : '.$infos['desc'] : '{{'.$macro.'}} : '.$infos['desc']
-			);
-		}
+		$rows = array_merge($rows, $this->recursive_sample($dataset,$level,$parent));
 		$temp_file = tempnam(sys_get_temp_dir(), mt_rand(0,10000));
+
 		self::write($temp_file, $rows);
 		$stream = file_get_contents($temp_file);
 		unlink($temp_file);
 		return $stream;
+	}	
+
+	public function recursive_sample($dataset,$level = 0,$parent=''){
+		$stream = array();
+		$parent = ($parent!=''?$parent.'.':'');
+		$indentation = str_repeat("\t", $level);
+		$titleSize = 18-($level*2);
+		$titleSize = $titleSize < 12 ? $titleSize:12;
+		foreach($dataset as $macro => $infos){
+
+			$infos['type'] = isset($infos['type']) ? $infos['type'] : '';
+			switch($infos['type']){
+				case 'list':
+					$stream[]= array(
+						'content'=>  $indentation.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').' (liste)',
+						'style' => array(
+							'bold' => true,
+							'font-size' => $titleSize
+						)
+					);
+					$stream[]= array('content'=>  $indentation.'{{#'.$parent.$macro.'}}');
+					if(is_array($infos['value']) && isset($infos['value'][0])) $stream=  array_merge($stream,self::recursive_sample($infos['value'][0],$level+1));
+					$stream[]= array('content'=> $indentation.'{{/'.$parent.$macro.'}}');
+				break;
+				case 'object':
+					$stream[]= array(
+						'content'=> $indentation.$parent.$macro.' '.(!empty($infos['label'])?': '.$infos['label']:''),
+						'style' => array(
+							'bold' => true,
+							'font-size' => $titleSize
+						)
+					);
+					$stream= array_merge( $stream,self::recursive_sample($infos['value'],$level+1,$parent.$macro));
+				break;
+				case 'image' : 
+					$stream[]= array('content'=> $indentation.'{{'.$parent.$macro.'::image}} : '.( !isset($infos['label']) ? '': $infos['label']));
+				break;
+				default : 
+					$stream[]= array('content'=> $indentation.'{{'.$parent.$macro.'}} : '.( !isset($infos['label']) ? '': $infos['label']));
+				break;
+			}
+		}
+		return $stream;
 	}
 
-
-
 	/** ROOT **/
 	//Fichier [Content_Types].xml
 	public static function content_types(){
@@ -104,7 +141,7 @@ class WordExport
 				}
 				$stream .= '</w:rPr>';
 			}
-			$stream .= '<w:t>'. htmlspecialchars($row['content'], ENT_COMPAT).'</w:t>';
+			if(isset($row['content'])) $stream .= '<w:t xml:space="preserve">'. htmlspecialchars($row['content'], ENT_COMPAT).'</w:t>';
 			$stream .= '</w:r></w:p>';
 		}
 
@@ -113,7 +150,6 @@ class WordExport
 		return $stream;
 	}
 
-
 	//Retourne les différentes balises de style
 	public static function custom_style_map($property=null, $value=null){
 		$stdFontSize = 22; //11pt * 2
@@ -164,6 +200,7 @@ class WordExport
 	 **/
 	public static function write($path, $rows){
 		$docx = new ZipArchive();
+		
 		if(file_exists($path)) unlink($path);
 		$docx->open($path, ZipArchive::CREATE);
 
@@ -195,28 +232,26 @@ class WordExport
 		$this->relationships = $this->zip->getFromName('word/_rels/document.xml.rels');
 	}
 
-	public function add_image($tag, $img, $return=false){
+	public function add_image($img){
 		//Unité utilisé en OOXML, 1px = 9525 EMU
 		$emu = 9525;
 
-		$pathParts = explode(SLASH, $img);
-		$imgParts = explode('.', end($pathParts));
-		preg_match('/(jpg|jpeg|png|gif)/', end($imgParts), $ext);
-		$ext = $ext[0];
-		$imgParts[array_search(end($imgParts), $imgParts)] = $ext;
-
-		$cType = in_array($ext, array('jpg', 'jpeg')) ? 'jpeg' : $ext;
-		$pathParts[array_search(end($pathParts), $pathParts)] = implode('.', $imgParts);
-		$imgUrl = implode(SLASH, $pathParts);
-		$img = strpos($imgUrl, __ROOT__) !== false ? $img : __ROOT__.$imgUrl;
+		$finfo = new finfo(FILEINFO_MIME);
+		$mime = $finfo->buffer($img);
+		
+		$ext = 'jpg';
+		switch($mime){
+			case 'image/jpeg': $ext = 'jpeg'; break;
+			case 'image/png': $ext = 'png'; break;
+			case 'image/gif': $ext = 'gif'; break;
+		}
+		if($mime == 'image/jpg') $mime = 'image/jpeg';
 
-		//Récupération taille image
-		list($width, $height) = getimagesize($img);
+		list($width, $height) = getimagesizefromstring($img);
 
 		$mimeTypes = $this->zip->getFromName('[Content_Types].xml', 0, ZipArchive::OVERWRITE);
-		if(strrpos($mimeTypes, '<Default Extension="'.$ext.'" ContentType="image/'.$cType.'"/>') === false) {
+		if(strrpos($mimeTypes, '<Default Extension="'.$ext.'" ContentType="'.$mime.'"/>') === false) {
 			$mimeTypes = str_replace('</Types>', '<Default Extension="gif" ContentType="image/gif"/><Default Extension="png" ContentType="image/png"/><Default Extension="jpeg" ContentType="image/jpeg"/><Default Extension="jpg" ContentType="image/jpeg"/></Types>', $mimeTypes);
-			// $mimeTypes = str_replace('</Types>', '<Default Extension="'.$ext.'" ContentType="image/'.$cType.'"/></Types>', $mimeTypes);
 			$this->zip->addFromString('[Content_Types].xml', $mimeTypes);
 		}
 
@@ -226,89 +261,137 @@ class WordExport
 			$i++;
 			$uid = 'img'.$i;
 		}
-		$this->zip->addFile($img, 'word/media/'.$uid.'.'.$ext);
+		$this->zip->addFromString( 'word/media/'.$uid.'.'.$ext,$img);
 
 		$rel = '<Relationship Id="'.$uid.'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/'.$uid.'.'.$ext.'"/>';
 		$this->relationships = str_replace('</Relationships>',$rel.'</Relationships>',$this->relationships);
 
 		$xmlPic = '<w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0" ><wp:extent cx="'.$width*$emu.'" cy="'.$height*$emu.'"/><wp:effectExtent l="1" t="1" r="1" b="1"/><wp:docPr id="'.rand(100,200).'" name="'.$uid.'" descr=""/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="0" name=""/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="'.$uid.'"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="'.$width*$emu.'" cy="'.$height*$emu.'"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing>';
 
-		$this->body = str_replace('{{'.$tag.'}}',$xmlPic, $this->body);
-		if($return){
-			$datas['picture'] = $xmlPic;
-			$datas['cType'] = $mimeTypes;
-			return $datas;
-		}
+		return $xmlPic;
 	}
 
-	//crée le document final avec les tags remplacés
-	public static function from_template($source, $data, $return){ 
-		$destination = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.docx';
-		$source = File::dir().$source;
-
-		copy($source,$destination);
-		$docx = new self();
-		$docx->parse($destination);
+	public function start(&$modelStream, $data){
+		$this->destination = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.docx';
+		file_put_contents($this->destination, utf8_decode($modelStream) );
+		$this->parse($this->destination);
+		$modelStream = $this->body;
+	}
 
-		foreach($data as $tag => $value){ 
+	public function from_template($stream, $datas){
+		//if / loop
+		$loopIfRegex = '/\{\{\#([^\/\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
+		$stream =  preg_replace_callback($loopIfRegex,function($matches) use ($datas) {
+			$key = $matches[1];
+			$streamTpl = $matches[2];
+			$keyInfos = explode('::',$key);
+			$key = $keyInfos[0];
+			$type = isset($keyInfos[1]) ? $keyInfos[1] : 'string';
+
+			$value = $this->decomposeKey($datas,$key);
+			
+			//gestion des boucles
 			if(is_array($value)){
-				$docx->body = preg_replace_callback('#\{\{\#'.$tag.'\}\}(.*)\{\{\/'.$tag.'\}\}#si', function ($match) use($value, $docx) {
-					$content = '';
-					foreach($value as $line){
-						$bloc = $match[1];
-						foreach($line as $subTag=>$subValue){
-							if(substr($subValue,0,2)=='::' && strpos($bloc, '{{'.$subTag.'}}') !== false) {
-								$picDatas = $docx->add_image($subTag, substr($subValue,2), true);
-								$bloc = str_replace('{{'.$subTag.'}}',$picDatas['picture'],$bloc);
-								continue;
-							}
-							$subValue = self::convert_text_format($subValue);
-							$bloc = str_replace('{{'.$subTag.'}}',$subValue,$bloc);
-						}
-						$content .=$bloc;
-					}
-					return $content;
-				},$docx->body);
-				continue;
+				$stream = '';
+				foreach($value as $line){
+					$localData = is_array($line) ? array_merge($datas,$line) : $datas;
+					$stream .= $this->from_template($streamTpl,$localData);
+				}
+				return $stream;
+			//gestion des if
+			}else{
+				return !isset($value) || (!is_array($value) && empty($value)) || (is_array($value) && count($value)==0) ? '' : $this->from_template($streamTpl,$datas);
 			}
+		},$stream);
+
+		//gestion des elses
+		$elseRegex = '/\{\{\^([^\/\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
+		$stream =  preg_replace_callback($elseRegex,function($matches) use ($datas) {
+			$key = $matches[1];
+			$streamTpl = $matches[2];
 
-			if(substr($value,0,2)=='::'){
-				$docx->add_image($tag, substr($value,2));
-				continue;
+			$keyInfos = explode('::',$key);
+			$key = $keyInfos[0];
+			$type = isset($keyInfos[1]) ? $keyInfos[1] : 'string';
+
+			$value = $this->decomposeKey($datas,$key);
+
+			if(is_array($value)){
+				$stream = '';
+				foreach($value as $line){
+					$localData = is_array($line) ? array_merge($datas,$line) : $datas;
+					$stream .= $this->from_template($streamTpl,$localData);
+				}
+				return $stream;
+			//gestion des else
+			}else{
+				return !isset($value) || (!is_array($value) && empty($value)) || (is_array($value) && count($value)==0) ? $this->from_template($streamTpl,$datas) : '';
 			}
-			$value = self::convert_text_format($value);
+		},$stream);
+
+		//gestion des simples variables
+	    $stream = preg_replace_callback('/{{([^#\/}]*)}}/',function($matches) use ($datas) {
+	        $key = $matches[1];
+
+	        $keyInfos = explode('::',$key);
+			$key = $keyInfos[0];
+			$type = isset($keyInfos[1]) ? $keyInfos[1] : 'string';
+
+	        $value = $this->decomposeKey($datas,$key);
+	        if(!isset($value)) return $this->formatValue($type,$matches[0]);
+	        if(is_array($value)) return 'Array';
+	        return $this->formatValue($type,$value);
+	    },$stream); 
+	    
+		return $stream;
+	}
 
-			$docx->body = str_replace('{{'.$tag.'}}',$value,$docx->body);
-			$docx->footer = str_replace('{{'.$tag.'}}',$value,$docx->footer);
-			$docx->header = str_replace('{{'.$tag.'}}',$value,$docx->header);
-		}
+	public  function formatValue($type,$value){
+		if($type == 'image') $value = $this->add_image($value);
+		if($type == 'html') $value = self::convert_text_format($value);
 	
-		//conditions - if
-		$ifRegex = '/\{\{\#([^\/\:\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
-		$docx->body = preg_replace_callback($ifRegex,function($matches) use ($data) {
-			$key = $matches[1];
-			$docx->body = $matches[2];
-			return !isset($data[$key]) || (!is_array($data[$key]) && empty($data[$key])) || (is_array($data[$key]) && count($data[$key])==0) ?'':$docx->body;
-		},$docx->body);
-		
-		//conditions - else
-		$elseRegex = '/\{\{\^([^\/\:\#}]*)\}\}(.*?)\{\{\/\1\}\}/is';
-		$docx->body = preg_replace_callback($elseRegex,function($matches) use ($data) {
-			$key = $matches[1];
-			$docx->body = $matches[2];
-			return (isset($data[$key]) && !is_array($data[$key]) && !empty($data[$key])) || (is_array($data[$key]) && count($data[$key])>0) ?'':$docx->body;
-		},$docx->body);
-
-		$docx->save();
-		$docx->close();
+		if($type == 'string'){
+			//Remplace les & par des &amp;
+			$value = str_replace(array('&'), array('&amp;'), $value);
+			//remplace les balises non word (ne commencant pas par <w: ou </w: )
+			$value = preg_replace('|<(?!/?w:)([^>]*)>|is', '&lt;$1&gt;', $value);
+		}
+		return $value;
+	}
 
-		if($return!='stream') return $destination;
+	public function end($stream,$data){
+		$this->body = $stream;
+		
+		$this->footer = $this->from_template($this->footer, $data);
+		$this->header = $this->from_template($this->header, $data);
 
-		$stream = file_get_contents($destination);
-		unlink($destination);
+		$this->zip->addFromString('word/document.xml', $this->body);
+		if(!empty($this->header)) $this->zip->addFromString('word/header1.xml', $this->header);
+		if(!empty($this->footer)) $this->zip->addFromString('word/footer1.xml', $this->footer);
+		$this->zip->addFromString('word/_rels/document.xml.rels', $this->relationships);
+		$this->zip->close();
+		
+		$stream = file_get_contents($this->destination);
+		unlink($this->destination);
 		return $stream;
 	}
 
+	public function decomposeKey($datas,$key){
+		if(array_key_exists($key, $datas)) return isset($datas[$key]) ? $datas[$key] : '' ;
+		
+		$attributes = explode('.',$key);
+		$current = $datas;
+		$value = null;
+
+        foreach ($attributes as $attribute) {
+        	if(!array_key_exists($attribute, $current))  break;
+        	$current = $current[$attribute];
+        	$value = isset($current) ?  $current :  '';
+        }
+        return $value;
+	}
+
+	// Converti les data wysiwyg en wysiwyg word
 	public static function convert_text_format($value){
 		//Remplace les <p> et les <br> par les w:br word
 		$value = str_replace(array('<br>','<p>','</p>','<ul>'),'<w:br/>',$value);
@@ -318,27 +401,9 @@ class WordExport
 		$value = str_replace('</ul>','',$value);
 		
         //todo - $value = preg_replace('|<strong>([^<]*)</strong>|is', '<w:r w:rsidRPr="00556DA8"><w:rPr><w:b /></w:rPr><w:t>$1</w:t></w:r>', $value);
-		
-		//Remplace les & par des &amp;
-		$value = str_replace(array('&'), array('&amp;'), $value);
-		//remplace les balises non word (ne commencant pas par <w: ou </w: )
-		$value = preg_replace('|<(?!/?w:)([^>]*)>|is', '&lt;$1&gt;', $value);
 		return $value;
 	}
 
-	//Ajoute au fichier les différentes modifications effectuées
-	public function save(){
-		$this->zip->addFromString('word/document.xml', $this->body);
-		if(!empty($this->header)) $this->zip->addFromString('word/header1.xml', $this->header);
-		if(!empty($this->footer)) $this->zip->addFromString('word/footer1.xml', $this->footer);
-		$this->zip->addFromString('word/_rels/document.xml.rels', $this->relationships);
-	}
-
-	//Ferme le fichier
-	public function close(){
-		$this->zip->close();
-	}
-
 	public static function strip($content){
 		$content = preg_replace_callback(
 			'#\{\{([^\]]+)\}\}#U',

+ 19 - 1
plugin/hackpoint/css/install.css

@@ -1,5 +1,5 @@
 html,body,.footer{
-	background-color:#343a40;
+	background-color:#343a40!important;
 	color:#fefefe;
 }
 
@@ -77,3 +77,21 @@ a {
 a:hover {
     color: #8fbcec;
 }
+
+.input-group-text {
+    display: -ms-flexbox;
+    display: flex;
+    -ms-flex-align: center;
+    align-items: center;
+    padding: .375rem .75rem;
+    margin-bottom: 0;
+    font-size: 1rem;
+    font-weight: 400;
+    line-height: 1.5;
+    color: #717d8a;
+    text-align: center;
+    white-space: nowrap;
+    background-color: #171a1d;
+    border: 1px solid #171a1d;
+    border-radius: .25rem;
+}

+ 3 - 2
plugin/hackpoint/css/main.css

@@ -440,9 +440,10 @@ div.hackpoint-type-image[data-type="dropzone"] > div{
     margin:5px 0!important;
     box-shadow: 0px 0px 20px rgba(0,0,0,0.5);
 }
-.hackpoint-type-image li > a{
+
+/*.hackpoint-type-image li > a{
 	display: none;
-}
+}*/
 
 div.hackpoint-type-image[data-type="dropzone"] > ul > li {
     vertical-align: top;

+ 1 - 1
plugin/hackpoint/install.php

@@ -1,6 +1,6 @@
 <?php 
 
-$enablePlugins = array('fr.idleman.hackpoint','fr.idleman.customiser','fr.idleman.part','fr.idleman.document','fr.idleman.notification','fr.sys1.wiki','fr.idleman.navigation');
+$enablePlugins = array('fr.idleman.hackpoint','fr.idleman.customiser','fr.idleman.part','fr.sys1.document','fr.sys1.notification','fr.sys1.wiki','fr.sys1.navigation');
 
 $conf->put('core_theme','/plugin/customiser/theme/hackpoint/main.css');
 $conf->put('wiki_name','Le wiki');

+ 4 - 4
plugin/hackpoint/page.sheet.sketch.php

@@ -23,7 +23,7 @@ if(isset($_['sidebar']) && $_['sidebar'] == 0) $sketchClasses .= ' no-sidebar';
 					<li data-id="{{id}}" class="hidden" onclick="hackpoint_resource_edit(this);">
 						<i class="far fa-trash-alt delete-resource" onclick="hackpoint_resource_delete(this,event)" ></i>
 						<i class="{{type.icon}}"></i>
-						<h3 title="Double cliquer pour modifier" ondblclick="hackpoint_resource_title_edit(event,this);"><span>{{label}}</span><input type="text" value="{{label}}" class="hidden"></h3>
+						<h3 data-tooltip data-tooltip data-placement="right" title="Double cliquer pour modifier" ondblclick="hackpoint_resource_title_edit(event,this);"><span>{{label}}</span><input type="text" value="{{label}}" class="hidden"></h3>
 						<small style="background:{{type.background}};color:{{type.color}}">{{type.label}}</small>
 					</li>
 				</ul>
@@ -31,7 +31,7 @@ if(isset($_['sidebar']) && $_['sidebar'] == 0) $sketchClasses .= ' no-sidebar';
 			
 
 			<div class="btn-group dropright w-100 resource-dropdown">
-			  <button type="button" class="btn btn-dark btn-add-resource" title="Ajouter une ressource" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+			  <button type="button" class="btn btn-dark btn-add-resource" data-tooltip data-placement="right" title="Ajouter une ressource" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
 			    <i class="fas fa-plus"></i>
 			  </button>
 			  <div class="dropdown-menu">
@@ -59,8 +59,8 @@ if(isset($_['sidebar']) && $_['sidebar'] == 0) $sketchClasses .= ' no-sidebar';
 								
 							</div>
 							<div class="btn-group mr-2" role="group" aria-label="Properties">
-								<label for="state" class="input-group-text pointer m-0">
-									<input id="state" name="state" class="form-control editable-input" onclick="hackpoint_sketch_save();" <?php echo $sketch->state?'checked="checked"':''; ?> type="checkbox" data-type="checkbox"> Public
+								<label for="state" class="input-group-text pointer m-0" data-tooltip data-placement="bottom" title="Rendre publique/privé">
+									<input id="state" name="state" class="form-control editable-input" onclick="hackpoint_sketch_save();" <?php echo $sketch->state?'checked="checked"':''; ?> type="checkbox" data-type="checkbox"> Publique
 								</label>
 							</div>
 							

+ 1 - 1
plugin/issue/IssueEvent.class.php

@@ -1,7 +1,7 @@
 <?php
 /**
  * Define a issueevent.
- * @author Administrateur
+ * @author Administrateur SYS1
  * @category Plugin
  * @license copyright
  */

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