Browse Source

Upgrade code

idleman 5 years ago
parent
commit
60f3d5a518
37 changed files with 2726 additions and 334 deletions
  1. 4 3
      class/Entity.class.php
  2. 15 0
      class/Plugin.class.php
  3. 1 1
      common.php
  4. 8 4
      constant-sample.php
  5. 1 1
      footer.php
  6. 1 1
      js/main.js
  7. 67 9
      js/plugins.js
  8. 25 12
      plugin/activedirectory/ActiveDirectory.class.php
  9. 99 29
      plugin/activedirectory/action.php
  10. 118 79
      plugin/activedirectory/activedirectory.plugin.php
  11. 6 22
      plugin/activedirectory/css/main.css
  12. 124 12
      plugin/activedirectory/js/main.js
  13. 138 152
      plugin/activedirectory/setting.activedirectory.php
  14. 1 1
      plugin/customiser/app.json
  15. 1 1
      plugin/customiser/theme/example/app.json
  16. 1 1
      plugin/example/js/main.js
  17. 1 1
      plugin/git/GitRepository.class.php
  18. 1 1
      plugin/git/app.json
  19. 58 0
      plugin/issue/IssueEvent.class.php
  20. 49 0
      plugin/issue/IssueReport.class.php
  21. 30 0
      plugin/issue/IssueReportTag.class.php
  22. 519 0
      plugin/issue/action.php
  23. 13 0
      plugin/issue/app.json
  24. 24 0
      plugin/issue/css/component.css
  25. 375 0
      plugin/issue/css/main.css
  26. 110 0
      plugin/issue/issue.plugin.php
  27. 70 0
      plugin/issue/js/component.js
  28. 5 0
      plugin/issue/js/html2canvas.min.js
  29. 306 0
      plugin/issue/js/main.js
  30. 103 0
      plugin/issue/mail.template.php
  31. 66 0
      plugin/issue/modal.issue.report.php
  32. 206 0
      plugin/issue/page.sheet.report.php
  33. 117 0
      plugin/issue/setting.global.report.php
  34. 22 0
      plugin/notification/action.php
  35. 11 0
      plugin/notification/js/main.js
  36. 14 1
      plugin/notification/notification.plugin.php
  37. 16 3
      plugin/notification/setting.notification.php

+ 4 - 3
class/Entity.class.php

@@ -748,10 +748,11 @@ require_once(__ROOT__.'class'.SLASH.'Database.class.php');
 
         $queryNumber = $query;
 
-        $queryNumber = preg_replace("/(SELECT.+[\n|\t]*)FROM[\s\t\r\n]/iU", 'SELECT DISTINCT '.$obj->tableName().'.'.$key.' FROM ',$queryNumber);
-
+         
+        $queryNumber = preg_replace("/(SELECT.+[\n|\t]*FROM[\s\t\r\n])({{table}}|`?".$obj->tableName()."`?)/iU", 'SELECT DISTINCT '.$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;

+ 15 - 0
class/Plugin.class.php

@@ -180,6 +180,21 @@ class Plugin{
 		}
 		return $stream;
 	}
+
+	/*
+	 Aide à l'inclusion de classes de plugin, ex:
+	 require_once('..'.SLASH.'plugin'.SLASH.'issue'.SLASH.'Event.class.php');
+	 	devient : 
+	 Plugin::need('issue/Event');
+	 Il est possible de faires des inclusions mulitples, ex :
+	 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');
+		}
+	}
 	
 }
 

+ 1 - 1
common.php

@@ -58,7 +58,7 @@ if($myUser->login==null && isset($_COOKIE[COOKIE_NAME])){
 	$cookie = UserPreference::load(array('key'=>'cookie','value'=>$_COOKIE[COOKIE_NAME]));
 
 	if($cookie!=false){
-	    if(Plugin::is_active('fr.idleman.activedirectory'))
+	    if(Plugin::is_active('fr.sys1.activedirectory'))
 	        require_once(PLUGIN_PATH.'activedirectory'.SLASH.'activedirectory.plugin.php');
 	    
 	    $myUser = User::byLogin($cookie->user);

+ 8 - 4
constant-sample.php

@@ -21,11 +21,15 @@ define('CRYPTKEY','{{CRYPT_KEY}}');
 //logs toutes les requetes sans formattage
 define('BASE_DEBUG',false);
 
-define('COOKIE_NAME','hackpoint-cookie');
+define('COOKIE_NAME','erp-core-cookie');
 
-define('PROGRAM_NAME','Hackpoint');
-define('PROGRAM_UID','idleman.fr/hackpoint');
-define('PROGRAM_TECHNICIAN','valentin.carruesco');
+define('PROGRAM_NAME','Sys1 ERP');
+define('PROGRAM_UID','sys1/erp-core');
+define('PROGRAM_TECHNICIAN','valentin.morreel');
+//Windows
+define('REFERENCE_URL','https://projet.sys1.fr/action.php?action=reference_save_project');
+//Linux
+// define('REFERENCE_URL','http://projet.sys1.fr/action.php?action=reference_save_project');
 
 define('SOURCE_VERSION','1.0');
 define('BASE_VERSION','1.0');

+ 1 - 1
footer.php

@@ -10,7 +10,7 @@ $cacheVersion = isset($cacheVersion) ? $cacheVersion : 1;
 ?>
 	<footer class="footer noPrint">
 		<div class="container">
-			<span class="text-muted"><?php echo PROGRAM_NAME.' V'.SOURCE_VERSION.'b'.BASE_VERSION; ?>  by <a href="<?php echo $scheme; ?>://idleman.fr" target="_blank">@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 PROGRAM_NAME.' V'.SOURCE_VERSION.'b'.BASE_VERSION; ?>  by <a href="<?php echo $scheme; ?>://sys1.fr" target="_blank">@Sys1</a> <!--| <a href="file/guide/manuel-utilisateur.pdf" target="_blank"><i class="far fa-file-pdf"></i> Documentation</a> -->-  <?php echo $loadingTime; ?></span>
 		</div>
 		<div id="toTheTop" title="Retour en haut de page"></div>
 	</footer>

+ 1 - 1
js/main.js

@@ -1374,7 +1374,7 @@ $(document).ready(function(){
 
 //TOGGLE FILTERS SEARCH
 function switch_advanced_filter(element){
-	$(element).closest('.filter-box').find('.advanced-search').slideToggle(100);
+	$(element).closest('.filter-box').find('.advanced-search').toggleClass('hidden');
 }
 
 // SEARCH

+ 67 - 9
js/plugins.js

@@ -25,7 +25,7 @@
 
 	//Affiche un message 'message' de type 'type' pendant 'timeout' secondes
 	$.message = function (type,message,timeout){
-		message = message.replace(/<\/?[script|iframe|object][^>]*>/gim,'');
+		message = message.replace(/<\/?script|iframe|object[^>]*>/gim,'');
 		$.toast({ type: type, content: message, timeout: timeout });
 	}
 
@@ -493,6 +493,7 @@ $.fn.extend({
 			var model = null;
 			var container = null;
 			option = $.extend({
+				differential : false,
 				showing : function(item){
 					//permet la personnalisation de l'apparition des lignes ( removeClass('hidden') par defaut)
 					item.removeClass('hidden');
@@ -513,24 +514,25 @@ $.fn.extend({
 			if(obj.prop("tagName") == 'UL'){
 				container = obj;
 				model = container.find('li:first-child');
-				if(!option.export) container.children('li:visible').remove();
+				if(!option.export && !option.differential) container.children('li:visible').remove();
 			} else if(obj.prop("tagName") == 'TABLE'){
 				container = obj.find('tbody');
 				model = container.find('tr:first-child');
-				if(!option.export) container.children('tr:visible').remove();
+				if(!option.export && !option.differential) container.children('tr:visible').remove();
 			} else if(obj.prop("tagName") == "SELECT"){
 				container = obj;
 				model = container.find('option[value*="{{"]');
 				if(model.length==0) model = $('<option class="hidden" value="{{value}}">{{label}}</option>');
-				if(!option.export) container.find('option:not([value*="{{"])').remove();
+				if(!option.export && !option.differential) container.find('option:not([value*="{{"])').remove();
 			} else{
 				container = obj;
 				childName = container.children().get(0).nodeName;
 				model = container.find(childName+':first-child');
-				if(!option.export) container.find(childName+':visible:not(.nofill)').remove();
+				if(!option.export && !option.differential) container.find(childName+':visible:not(.nofill)').remove();
 			}
 			var tpl = model.get(0).outerHTML;
-			
+
+
 			//fix jquery backslashes break
 			tpl = tpl.replace(/{{##/g,'{{/').replace(/{{\/(.*)}}=""/g,'{{/$1}}');
 		
@@ -548,16 +550,72 @@ $.fn.extend({
 			//on clone l'objet option pour ne transmettre que des datas utiles
 			data = $.extend({},option);
 			delete data.showing;
+			delete data.templating;
 
 			$.action(data,function(r){
 				//On ne gere la pagination et l'affichage tableau que si on est pas en mode export
 				if(!option.export){
+
+					var activeIds = [];
 					for(var key in r.rows){
-						var line = $(Mustache.render(tpl,r.rows[key]));
-						container.append(line);
-						option.showing(line,key);
+
+						var line;
+						var data = r.rows[key];
+						var lineTpl = tpl;
+						if(option.templating) lineTpl = option.templating(data,line,tpl);
+
+						if(!option.differential){
+							line = $(Mustache.render(lineTpl,data));
+							container.append(line);
+							option.showing(line,key);
+						}else{
+							activeIds.push(data.id);
+							var existing = $('> [data-id="'+data.id+'"]',container);
+							//existe en data et pas dans le dom : ajout
+							if(existing.length == 0){
+								line = $(Mustache.render(lineTpl,data));
+								line.attr('data-update-tag',data.updated);
+								
+								if(key==0){
+									container.append(line);
+								}else{
+									var previousIndex = key-1;
+									var previous = $('>[data-id]:visible',container).eq(previousIndex);
+									previous.after(line);
+								}
+								option.showing(line,key);
+							}else{
+								//existe en data et dans le dom et pas de modification : on passe au suivant
+								if(existing.attr('data-update-tag') == data.updated){
+									continue;
+								//existe en data et dans le dom mais a été modifié  : on remplace
+								}else{
+									line = $(Mustache.render(lineTpl,data));
+									line.attr('data-update-tag',data.updated);
+									existing.after(line);
+									existing.remove();
+									option.showing(line,key);
+								}
+							}
+						}
+					}
+
+					//suppression des élements dom qui ne sont plus en db
+					if(option.differential){
+						$('>[data-id]:visible',container).each(function(i,line){
+							
+							var line = $(line);
+							
+							
+							if(activeIds.indexOf(line.attr('data-id')) == -1){
+								console.log(line.attr('data-id'));
+								console.log('suppression ',line);
+								line.remove();
+							}
+						});
 					}
 
+
 					if(r.pagination){
 						$('.page-item-previous,.page-item-next').remove();
 						r.pagination.pages = Math.ceil(r.pagination.pages);

+ 25 - 12
plugin/activedirectory/ActiveDirectory.class.php

@@ -23,14 +23,13 @@ class ActiveDirectory
 	 */
 	
 	public function connect($login=false,$password=false){
-		
 		putenv('LDAPTLS_REQCERT=never');
-		if($this->server==null || $this->port==null || $this->userRoot==null) throw new Exception('Paramêtres de connexion manquants');
+		if($this->server==null || $this->port==null || $this->userRoot==null) throw new Exception('Paramètres de connexion manquants',400);
 		$this->datasource = ldap_connect($this->server,$this->port);
-		if (!$this->datasource) throw new Exception('Connexion échouée');	
+		if(!$this->datasource) throw new Exception('Connexion échouée', 400);	
 		ldap_set_option($this->datasource,LDAP_OPT_PROTOCOL_VERSION,$this->protocolVersion);
 		ldap_set_option($this->datasource, LDAP_OPT_REFERRALS, 0);
-		if(@ldap_bind($this->datasource,$login,$password) ==false) throw new Exception('Identifiant ou mot de passe incorrect');
+		if(@ldap_bind($this->datasource,$login,$password) == false) throw new Exception('Identifiant ou mot de passe incorrect', 401);
 	}
 	
 
@@ -72,15 +71,29 @@ class ActiveDirectory
 
 	
 	/**
-	 * Recherche des valeurs dans la base de données en fonction du filtre
+	 * 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
 	 * @return <Array> tableau contenant les objets correspondants a la recherche
 	 */
-	public function search($dn,$filtre){
-		$sr=ldap_search($this->datasource, $dn, $filtre);
-		$info = ldap_get_entries($this->datasource, $sr);
-		return $info;
+	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;
 	}
 	
 	/**
@@ -88,8 +101,7 @@ class ActiveDirectory
 	 * @param <String> Racine contexte de la recherche
 	 */
 	public function populate($dn){
-		
-		return $this->search($dn,"(samAccountName=*)");
+		return $this->search($dn,"(&(samAccountName=*)(objectClass=user))");
 	}
 
 	public function get_domain_name($login){
@@ -109,7 +121,7 @@ class ActiveDirectory
 	}
 
 	public function change_password( $userDn , $newPassword ) {
-		return ldap_mod_replace($this->datasource, $userDn ,  self::encrypt_password($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 ) {
@@ -122,6 +134,7 @@ class ActiveDirectory
 		return array("unicodePwd" => $newPassw);
 	}
 	
+
 	/**
 	 * Deconnexion du LDAP
 	 */

+ 99 - 29
plugin/activedirectory/action.php

@@ -1,30 +1,101 @@
 <?php 
-require_once __DIR__.SLASH.'ActiveDirectoryGroup.class.php';
+require_once(__DIR__.SLASH.'ActiveDirectoryGroup.class.php');
 global $_,$conf,$myUser;
 switch($_['action']){
-	case 'activedirectory_activedirectory_settings_save':
-		if($myUser->can('activedirectory','configure')){
-			$conf->put('plugin_activedirectory_server',$_['ad-server']);
-			$conf->put('plugin_activedirectory_port',$_['ad-port']);
-			$conf->put('plugin_activedirectory_ssl_port',$_['ad-ssl-port']);
-			$conf->put('plugin_activedirectory_user_root',$_['ad-user-root']);
-			$conf->put('plugin_activedirectory_group_root',$_['ad-group-root']);
-			$conf->put('plugin_activedirectory_domain',$_['ad-domain']);
-
-			$conf->put('plugin_activedirectory_reader_login',$_['ad-reader-login']);
-			$conf->put('plugin_activedirectory_reader_password',$_['ad-reader-password']);
-			
-			$conf->put('plugin_activedirectory_admin_login',$_['ad-admin-login']);
-			$conf->put('plugin_activedirectory_admin_password',$_['ad-admin-password']);
-
-		}
-		header('location: setting.php?section=activedirectory&success=Configuration enregistrée');
+	case 'activedirectory_setting_save':
+		Action::write(function(&$response){
+			global $myUser,$conf,$_;
+			User::check_access('activedirectory','configure');
+
+			foreach(Configuration::setting('activedirectory') as $key=>$value){
+				if(!is_array($value) && !in_array($key, array('activedirectory_users_root', 'activedirectory_groups_root'))) continue;
+				$allowed[] = $key;
+			}
+
+			foreach ($_['fields'] as $key => $value){
+				if(in_array($key, $allowed)){
+					if(in_array($key, array('activedirectory_users_root', 'activedirectory_groups_root'))){
+						foreach ($value as $i => $val)
+							if(empty($val)) unset($value[$i]);
+						$value = implode(";",$value);
+					}
+					$conf->put($key,$value);
+				}
+			}
+			unset($_SESSION['configuration']);
+			$conf = new Configuration();
+			$conf->getAll();
+		});
+	break;
+
+	case 'activedirectory_connection_check':
+		Action::write(function(&$response){
+			global $myUser, $conf;
+			User::check_access('activedirectory','configure');
+
+			foreach (array('reach','reader','users') as $check)
+				$response['tests'][$check.'-connection'] = false;
+
+			try {
+				$ldap = ldap_instance();
+				$ldap->connect($conf->get('activedirectory_reader_login'),$conf->get('activedirectory_reader_password'));
+				$response['tests']['reach-connection'] = true;
+				
+				if(!empty($conf->get('activedirectory_reader_login')) && !empty($conf->get('activedirectory_reader_password')))
+					$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;
+				$ldap->disconnect();
+			} catch (Exception $e) {
+				switch ($e->getCode()) {
+					//Connexion simple
+					case 400:
+						$response['tests']['reach-connection'] = false;
+					break;
+					//Connexion compte reader
+					case 401:
+						$response['tests']['reader-connection'] = false;
+					break;
+					default:
+					break;
+				}
+				$ldap->disconnect();
+			}
+		});
 	break;
 
-	case 'activedirectory_activedirectory_save_link':
+	/** ACTIVEDIRECTORY GROUPS **/
+	case 'activedirectory_group_search':
+		Action::write(function(&$response){
+			global $myUser, $conf, $_;
+			User::check_access('activedirectory','read');
+			$query = 'SELECT * FROM '.ActiveDirectoryGroup::tableName().' WHERE 1';
+			$data = array();
+
+			$firms = array();
+			foreach(Firm::loadAll() as $firm)
+				$firms[$firm->id] = $firm->label;
+
+			$ranks = array();
+			foreach(Rank::loadAll() as $rank)
+				$ranks[$rank->id] = $rank->label;
+
+			$response['pagination'] = ActiveDirectoryGroup::paginate(10,(!empty($_['page'])?$_['page']:0),$query,$data);
+			foreach(ActiveDirectoryGroup::staticQuery($query,$data,true) as $adGroup){
+				$row = $adGroup->toArray();
+				$row['rankLabel'] = isset($ranks[$adGroup->rank]) ? $ranks[$adGroup->rank] : '-';
+				$row['firmLabel'] = isset($firms[$adGroup->firm]) ? $firms[$adGroup->firm] : '-';
+				$response['rows'][] = $row;
+			}
+		});
+	break;
+
+	case 'activedirectory_group_save':
 		Action::write(function(&$response){
 			global $myUser,$_;
-			if(!$myUser->can('activedirectory','edit')) throw new Exception("Permissions insuffisantes",403);
+			User::check_access('activedirectory','edit');
 			if(!isset($_['ad-group']) || empty($_['ad-group'])) throw new Exception("Nom de groupe obligatoire");
 
 			$item = ActiveDirectoryGroup::provide();
@@ -32,14 +103,13 @@ switch($_['action']){
 			$item->rank = $_['ad-rank'];
 			$item->firm = $_['ad-firm'];
 			$item->save();
-
 		});
 	break;
 
-	case 'activedirectory_activedirectory_edit_link':
+	case 'activedirectory_group_edit':
 		Action::write(function(&$response){
 			global $myUser,$_;
-			if(!$myUser->can('activedirectory','edit')) throw new Exception("Permissions insuffisantes",403);
+			User::check_access('activedirectory','edit');
 			$adgroup = ActiveDirectoryGroup::getById($_['id']);
 			$adgroup = $adgroup->toArray();
 			$adgroup['ad-group'] = html_entity_decode($adgroup['adgroup']);
@@ -49,11 +119,11 @@ switch($_['action']){
 		});
 	break;
 
-	case 'activedirectory_activedirectory_delete_link':
-		global $_;
-		if(!$myUser->can('activedirectory','delete')) throw new Exception("Permissions insuffisantes",403);
-		ActiveDirectoryGroup::deleteById($_['groupId']);
-
-		header('location: setting.php?section=activedirectory&info=Correspondance Groupe/Rang/Établissement supprimée');
+	case 'activedirectory_group_delete':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('activedirectory','delete');
+			ActiveDirectoryGroup::deleteById($_['id']);
+		});
 	break;
 }

+ 118 - 79
plugin/activedirectory/activedirectory.plugin.php

@@ -1,9 +1,9 @@
 <?php
 /*
 @name Connexion AD (LDAP)
-@author Valentin CARRUESCO <valentin.carruesco@idleman.fr>
-@link http://www.idleman.fr
-@licence Copyright IdleCorp
+@author Valentin CARRUESCO <valentin.carruesco@sys1.fr>
+@link http://www.sys1.fr
+@licence Copyright Sys1
 @version 1.0.0
 @description Plugin pour l'identification sur Active Directory (LDAP)
 */
@@ -11,15 +11,15 @@
 
 
 //Recuperation d'un instance ldap avec les configuraiton serveur
-function ldap_instance(){
+function ldap_instance($ssl = false){
 	require_once(__DIR__.SLASH.'ActiveDirectory.class.php');
 	global $conf;
 	$ldap = new ActiveDirectory();
-	$ldap->server = $conf->get('plugin_activedirectory_server');
-	$ldap->port = $conf->get('plugin_activedirectory_port');
-	$ldap->userRoot = $conf->get('plugin_activedirectory_user_root');
-	$ldap->groupRoot = $conf->get('plugin_activedirectory_group_root');
-	$ldap->domain = $conf->get('plugin_activedirectory_domain');
+	$ldap->server = ($ssl ? 'ldaps://':'' ).$conf->get('activedirectory_server');
+	$ldap->port = $ssl ? $conf->get('activedirectory_ssl_port'):  $conf->get('activedirectory_port');
+	$ldap->userRoot = $conf->get('activedirectory_users_root');
+	$ldap->groupRoot = $conf->get('activedirectory_groups_root');
+	$ldap->domain = $conf->get('activedirectory_domain');
 	$ldap->protocolVersion = 3;
 	return $ldap;
 }
@@ -28,13 +28,11 @@ function ldap_instance(){
 function ldap_plugin_all_users(&$users, $loadRights=false){
 	require_once(__DIR__.SLASH.'ActiveDirectory.class.php');
 	global $conf;
-
-	if(empty($conf->get('plugin_activedirectory_reader_login')) || empty($conf->get('plugin_activedirectory_reader_password')) || empty($conf->get('plugin_activedirectory_user_root')) ) return;
-
 	try{
 		$ldap = ldap_instance();
-		$ldap->connect($conf->get('plugin_activedirectory_reader_login'),$conf->get('plugin_activedirectory_reader_password'));	
-		$infos = $ldap->populate($conf->get('plugin_activedirectory_user_root'));
+		$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();
 		$allUsers = array();
 		foreach($infos as $info){
@@ -42,7 +40,6 @@ function ldap_plugin_all_users(&$users, $loadRights=false){
 				$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){
@@ -52,17 +49,14 @@ function ldap_plugin_all_users(&$users, $loadRights=false){
 				}
 				$newUser->manager = $manager;
 				$allUsers[] = $newUser;
-				
 			}
 		}
-
 		$users = $allUsers;
 	}catch(Exception $e){
 		$ldap->disconnect();
-		throw $e;
+		//Décommenter la ligne qui suit pour avoir un message d'erreur si pb de connexion à l'AD
+		//throw new Exception("Une erreur est survenue lors de la connexion à l'AD");
 	}
-
-	
 }
 
 //Récuperation d'un utilisateur précis en LDAP (appellé par User::check)
@@ -74,23 +68,24 @@ function ldap_plugin_identification(&$user,$login,$password,$loadRight,$loadMana
 	$ldap = ldap_instance();
 	try{
 		if($noPassword){
-			$ldap->connect($conf->get('plugin_activedirectory_reader_login'), $conf->get('plugin_activedirectory_reader_password'));
+			$ldap->connect($conf->get('activedirectory_reader_login'), $conf->get('activedirectory_reader_password'));
 		}else{
 			$ldap->connect($login.$ldap->domain, $password);
 		}
-		$infos = $ldap->search($conf->get('plugin_activedirectory_user_root'),"(userprincipalname=".$login.$ldap->domain.")");
+		
+		$infos = $ldap->search($conf->get('activedirectory_users_root'),"(&(userprincipalname=".$login.$ldap->domain.")(objectClass=user))");
 		
 		if($infos["count"]>0){
 			$user = new User();
 			ldap_user_fill($ldap,$user,$infos[0],$loadRight,$loadManager);
 			user_rank_firm_by_group($user);
 		}
-		$avatarPath = __ROOT__.FILE_PATH.AVATAR_PATH.$user->login.'.jpg';
-        if(!file_exists($avatarPath) && 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']));
-        }
 
+		$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
 	}
@@ -106,10 +101,10 @@ function ldap_user_fill($ldap,&$user,$infos,$loadRight=false,$loadManager = fals
 		//Convertion en seconds
 		$seconds = (float)($infos['accountexpires'][0] / 10000000); 
 		//Convertion d'un timestamp AD en timestamp UNIX
-		$timestamp = round($seconds - (((1970-1601) * 365.242190) * 86400)); 
+		$timestamp = round($seconds - (((1970-1601) * 365.242190) * 86400));
 	    if($timestamp <= time()) return;
     }
-
+    
 	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]);
@@ -122,7 +117,7 @@ function ldap_user_fill($ldap,&$user,$infos,$loadRight=false,$loadManager = fals
 	if(isset($infos['jpegphoto'][0])) $user->meta['ldap_avatar'] = base64_encode($infos['jpegphoto'][0]);
 	
 	global $conf;
-	$metafields = explode(PHP_EOL,$conf->get('plugin_activedirectory_metafields'));
+	$metafields = explode(PHP_EOL,$conf->get('activedirectory_metafields'));
 	foreach ($metafields as $line) {
 		$metaInfos = explode(':',$line);
 		if(count($metaInfos)<4) continue;
@@ -143,13 +138,13 @@ function ldap_user_fill($ldap,&$user,$infos,$loadRight=false,$loadManager = fals
 			if($managerEntry['count'] > 0 ){
 				$manager = new User();
 				ldap_user_fill($ldap,$manager,$managerEntry[0],$loadRight,false);
-				if(isset($infos['sn'][0])) $manager->setName($managerEntry[0]['sn'][0]);
-				if(isset($infos['givenname'][0])) $manager->setFirstName($managerEntry[0]['givenname'][0]);
-				if(isset($infos['mail'][0])) $manager->setMail($managerEntry[0]['mail'][0]);
-				if(isset($infos['telephonenumber'][0])) $manager->setPhone($managerEntry[0]['telephonenumber'][0]);
-				if(isset($infos['mobile'][0])) $manager->setMobile($managerEntry[0]['mobile'][0]);
-				if(isset($infos['title'][0])) $manager->function = $managerEntry[0]['title'][0];
-				if(isset($infos['samaccountname'][0])) $manager->login = mb_strtolower($managerEntry[0]['samaccountname'][0]);
+				if(isset($managerEntry['sn'][0])) $manager->setName($managerEntry[0]['sn'][0]);
+				if(isset($managerEntry['givenname'][0])) $manager->setFirstName($managerEntry[0]['givenname'][0]);
+				if(isset($managerEntry['mail'][0])) $manager->setMail($managerEntry[0]['mail'][0]);
+				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]);
 				$user->manager = $manager;
 			}
 		}
@@ -161,15 +156,15 @@ function ldap_user_fill($ldap,&$user,$infos,$loadRight=false,$loadManager = fals
 	if($loadRight){
 		$groups = array();
 		if(isset($infos['memberof'])){
-				for($i=0; $i<count($infos['memberof'])-1; ++$i){
-					$groupCN = $infos['memberof'][$i];
-					list($group,$root) = explode(',',$groupCN);
-					list($entity,$group) = explode('=',$group);
-					//TODO decommenter une fois les pb de perf résolus
-					//$ldap->recursiveGroups($groups,$groupCN);
-					$groups[] = $group; 
-				}
+			for($i=0; $i<count($infos['memberof'])-1; ++$i){
+				$groupCN = $infos['memberof'][$i];
+				list($group,$root) = explode(',',$groupCN);
+				list($entity,$group) = explode('=',$group);
+				//TODO decommenter une fois les pb de perf résolus
+				//$ldap->recursiveGroups($groups,$groupCN);
+				$groups[] = $group; 
 			}
+		}
 		$user->groups = $groups;
 	}
 
@@ -181,32 +176,49 @@ 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");
 	
-	if(json_encode($userForm->meta) != json_encode($user->meta)) throw new Exception("Cette fonctionnalité n'est pas disponible pour des utilisateurs active directory");
-	
-
 	global $_,$conf;
 	require_once(__DIR__.SLASH.'ActiveDirectory.class.php');
 
+	//Régles de définition de mot de passe
+	if(!empty($userForm->password)){
+		if(strlen($userForm->password)<7) throw new Exception("Le mot de passe doit être supérieur à 7 caractères");
+		if(!preg_match('|[0-9]|i', $userForm->password)) throw new Exception("Le mot de passe doit contenir au moins un chiffre");
+		if(!preg_match('|[a-z]|i', $userForm->password)) throw new Exception("Le mot de passe doit contenir au moins une lettre");
+		if(!preg_match('|[a-z]|', $userForm->password)) throw new Exception("Le mot de passe doit contenir au moins une lettre Minuscule");
+		if(!preg_match('|[A-Z]|', $userForm->password)) throw new Exception("Le mot de passe doit contenir au moins une lettre Majuscule");
+	}
 
-	$response['warning'] = 'Vous êtes sur un compte de société, seules les informations suivantes ont été modifiées :<br/>
-		- Téléphone<br/>
-		- Mobile<br/>';
-
-    $ldap = ldap_instance();
-    if($conf->get('plugin_activedirectory_admin_login')=='') throw new Exception("Le compte AD admin n'est pas configuré, veuillez contacter un administrateur");
-	$ldap->connect($conf->get('plugin_activedirectory_admin_login'),$conf->get('plugin_activedirectory_admin_password'));	
+    $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);
-	if(!$cn) throw new Exception("Impossible de trouver l'utilsateur dans la base AD");
+	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);
+	
+	if(in_array('mobile', $infos[0]))
+		$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));
+	}
 
+	if(!empty($userForm->password)){
+		$ldap->change_password($cn,$userForm->password);
+	}
 
-
-	$ldap->set($cn,'telephoneNumber',$userForm->phone);
-	$ldap->set($cn,'mobile',$userForm->mobile);
-	
+	$response['warning'] = 'Vous êtes sur un compte de société, seules les informations suivantes ont été modifiées :<br/>
+		- Téléphone<br/>
+		- Mobile<br/>
+		- Mot de passe (7 caracteres minimum : Majuscules, minucules et chiffres)<br/>
+		- Avatar (JPG uniquement)<br/>';
 
 	$ldap->disconnect();
 }
@@ -217,20 +229,24 @@ function user_rank_firm_by_group(&$user){
 	$firms = array();
 	$ranks = array();
 
-	$groups = ActiveDirectoryGroup::loadAll(array(), null,  null, array('*'),1);
-	if(empty($groups)) throw new Exception("Etablissements et accès non paramétrés, veuillez contacter un administrateur");
-	
 	if(!isset($user->groups)) $user->groups = array();
-	foreach($groups as $group){
+	foreach(ActiveDirectoryGroup::loadAll(array(), null,  null, array('*'),1) as $group){
 		if(!in_array($group->adgroup,$user->groups)) continue;
 		$firm = $group->join('firm');
 		$rank = $group->join('rank');
 		$firms[$firm->id] = $firm;
 		if(!isset($ranks[$firm->id])) $ranks[$firm->id] = array();
-		$ranks[$firm->id][] = $rank;
+		$ranks[$firm->id][$rank->id] = $rank;
 	}
 
-	if (!empty($ranks)) {
+	//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'));
+	}
+
+	if(!empty($ranks)) {
 		$user->setFirms($firms);
 		$defaultFirm = !empty($user->preference('default_firm')) ? $user->preferences['default_firm'] : key($firms);
 		$myFirm = $firms[$defaultFirm];
@@ -282,36 +298,59 @@ function activedirectory_directory_list(&$usermapping){
 		$user = $infos['object'];
 		//todo à dynamiser en fct de plugin_activedirectory_metafields
 		if(isset($user->meta['personalPhone'])) $usermapping[$login]['values']['Portable (perso)'] = '<a href="tel: '.$user->meta['personalPhone'].'">'.$user->meta['personalPhone'].'</a>';
+		// if(isset($user->meta['jobstart'])) $usermapping[$login]['values']['Date début contrat'] = $user->meta['jobstart'];
 	}
 }
 
 function activedirectory_account_global(){
 	global $myUser,$conf;
-	$metafields = explode(PHP_EOL,$conf->get('plugin_activedirectory_metafields'));
-	?>
+	$metafields = explode(PHP_EOL,$conf->get('activedirectory_metafields')); ?>
+	
 	<div class="row">
-	<?php
-	foreach ($metafields as $line) :
+	<?php foreach ($metafields as $line):
 		$metaInfos = explode(':',$line);
 		if(count($metaInfos)<4) continue;
 		list($label,$type,$adslug,$slug) = $metaInfos;
 	?>
-	
 		<div class="col-md-6">
-			<label for="<?php echo $slug; ?>"><?php echo $label ?>:</label>
+			<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]:''; ?>">
 		</div>
-	
-	<?php
-	endforeach;
-	?>
+	<?php endforeach; ?>
 	</div>
 	<?php
 }
 
-
-Plugin::addJs('/js/main.js?v=1.0');
-Plugin::addCss('/css/main.css?v=1.0');
+//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(
+    "Configuration de l'AD",
+    'activedirectory_server' => array("label"=>"Serveur","type"=>"text","legend"=>"L'adresse IP du serveur AD","placeholder"=>"192.168.XXX.XXX"),
+    'activedirectory_port' => array("label"=>"Port","type"=>"number","legend"=>"Le port sur lequel attaquer le serveur AD","placeholder"=>"389"),
+    'activedirectory_ssl_port' => array("label"=>"Port SSL","type"=>"number","legend"=>"Le port SSL sur lequel attaquer le serveur AD","placeholder"=>"636"),
+    '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")),
+    "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")
+));
+
+
+
+Plugin::addJs('/js/main.js');
+Plugin::addCss('/css/main.css');
 
 Plugin::addHook('directory_list',"activedirectory_directory_list");
 Plugin::addHook("account_global", "activedirectory_account_global"); 

+ 6 - 22
plugin/activedirectory/css/main.css

@@ -1,32 +1,16 @@
-#ad-correspondance label {
-	display: inline-block;
-}
-
-#ad-correspondance label:not(:first-of-type), #ad-correspondance div.btn {
-	margin-left: 15px;
-}
-
-#ad-correspondance div.btn.btn-success {
-	position: absolute;
-	right: 15px;
-	bottom: 0px;
-	margin-left: 0;
-}
 /* TABLE CORRESPONDANCE GROUPE / RANG / ETABLISSEMENT */
 .table-group-rank-firm {
 	vertical-align: middle;
-	margin-top: 30px;
 }
 .table-group-rank-firm .action-buttons{
 	width: 80px;
 	text-align: center
 }
 
-@media (max-width: 768px){
-	#active-directory-form-settings .row > div:not(last-of-type) {
-		margin-bottom: 15px;
-	}
-	#ad-correspondance div.btn.btn-success {
-		position: unset;
-	}
+.activedirectory-test-connection .card-body {
+	min-height: 266px;
+}
+
+.activedirectory-test-connection .text-fullbreak {
+	word-break: break-all;
 }

+ 124 - 12
plugin/activedirectory/js/main.js

@@ -1,23 +1,135 @@
-// SAVE
-function activedirectory_activedirectory_save_link(){
-	var data = $.getForm('#ad-correspondance');
-	data.id = $('#ad-correspondance').attr('data-id');
+//Init des settings
+function init_setting_activedirectory(parameter){
+	switch(parameter.section){
+		case 'activedirectory':
+			$(document).ready(function(){
+				var form = $('#activedirectory-setting-form');
+
+				//usersRoot
+				var usersInput = $('input[data-root="users"]', form);
+				if(usersInput.length>0){
+					$.each(usersInput.val().split(';'), function(i, value){
+						var usersClone = i==0 ? usersInput : usersInput.clone();
+						usersClone.val(value);
+						if(i!=0) usersInput.after(usersClone);
+					});
+				}
+
+				//groupsRoot
+				var groupsInput = $('input[data-root="groups"]', form);
+				if(groupsInput.length>0){
+					$.each(groupsInput.val().split(';'), function(i, value){
+						var groupsClone = i==0 ? groupsInput : groupsInput.clone();
+						groupsClone.val(value);
+						if(i!=0) groupsInput.after(groupsClone);
+					});
+				}
+			});
+			activedirectory_group_search();
+		break;
+	}
+}
+
+/* ACTIVEDIRECTORY GROUP */
+//SEARCH
+function activedirectory_group_search(callback){
+	$('#group-rank-firm').fill({
+		action:'activedirectory_group_search',
+	}, function(){
+		if(callback!=null) callback();
+	});
+}
+//SAVE
+function activedirectory_group_save(){
+	var form = $('#ad-correspondance');
+	var data = $.getForm(form);
+	data.id = form.attr('data-id');
+
 	$.action(data,function(r){
 		$.message('success','Enregistré');
-		$('#ad-correspondance input').val('');
-		$('#ad-correspondance').attr('data-id','');
-		$('#active-directory-form-settings').load(document.URL +  ' #active-directory-form-settings');
+		activedirectory_group_search();
+		$('input, select', form).val('');
+		form.attr('data-id','');
 	});
 }
-
-// EDIT
-function activedirectory_activedirectory_edit_link(element){
+//EDIT
+function activedirectory_group_edit(element){
 	var line = $(element).closest('tr');
 	$.action({
-		action:'activedirectory_activedirectory_edit_link',
+		action:'activedirectory_group_edit',
 		id:line.attr('data-id')
 	},function(r){
 		$.setForm('#ad-correspondance',r);
 		$('#ad-correspondance').attr('data-id',r.id);
 	});
-}
+}
+//DELETE
+function activedirectory_group_delete(element){
+	if(!confirm('Êtes-vous sûr de vouloir supprimer cette correspondance ?')) return;
+	var line = $(element).closest('tr');
+	$.action({
+		action: 'activedirectory_group_delete',
+		id: line.attr('data-id')
+	},function(r){
+		line.remove();
+		$.message('info','Correspondance Groupe/Rang/Établissement supprimée');
+	});
+}
+
+
+/* ACTIVEDIRECTORY SETTING */
+// ADD USER/GROUP ROOTS
+function activedirectory_activedirectory_add_roots(element){
+	var btn = $(element);
+	var input = btn.parent().next('td').find('input:eq(0)');
+	var clone = input.clone();
+
+	clone.val('');
+	input.parent().append(clone);
+}
+
+//SAVE
+function activedirectory_setting_save(callback){
+	//On est obligé de passer par là pour utiliser
+	//le tableau de configuration auto-généré avec
+	//les champs un peu customs
+	var fields = $('#activedirectory-setting-form').toJson();
+	fields['activedirectory_users_root'] = [];
+	$.each($('input[data-root="users"]'), function(i, input){
+		// if(!$(input).val().length) return;
+		fields['activedirectory_users_root'].push($(input).val());
+	});
+	fields['activedirectory_groups_root'] = [];
+	$.each($('input[data-root="groups"]'), function(i, input){
+		// if(!$(input).val().length) return;
+		fields['activedirectory_groups_root'].push($(input).val());
+	});
+
+	$.action({
+		action: 'activedirectory_setting_save',
+		fields: fields
+	}, function(r){
+		$.message('success', 'Enregistré');
+		if(callback!=null) callback();
+	});
+}
+
+//CHECK CONNECTION
+function activedirectory_connection_check(){
+	activedirectory_setting_save(function(){
+		$('.activedirectory-test-connection').load(document.URL +  ' .activedirectory-test-connection>*', function(){
+			$('.activedirectory-test-connection .card-icon > i').attr('class', 'fas fa-spinner fa-pulse ');
+			$.action({
+				action: 'activedirectory_connection_check'
+			}, function(r){
+				$.each(r.tests, function(id, result){
+					var card = $('#'+id);
+					card.addClass(result?'border-success':'border-danger');
+					$('.card-icon > i', card).attr('class', (result?'text-success fas fa-check-circle':'text-danger fas fa-times-circle'));
+					$('.label', card).addClass(result?'text-success':'text-danger');
+				});
+			});
+		});
+	});
+
+}

+ 138 - 152
plugin/activedirectory/setting.activedirectory.php

@@ -1,165 +1,151 @@
 <?php 
 global $myUser,$_,$conf;
-if(!$myUser->can('activedirectory','configure')) throw new Exception("Permissions insuffisantes",403);
-			require_once(__DIR__.SLASH.'ActiveDirectoryGroup.class.php');
+User::check_access('activedirectory','configure');
+require_once(__DIR__.SLASH.'ActiveDirectoryGroup.class.php');
+
 ?>
 <div class="row">
-	<div class="col-md-12">
-		<br>
+	<div class="col-md-12"><br>
+		<div class="btn btn btn-success ml-2 float-right" onclick="activedirectory_setting_save();"><i class="fas fa-check"></i> Enregistrer</div>
 		<h3>Réglages Active Directory</h3>
-		<hr/>
-		<?php 
-		if($myUser!=false){ ?>
-			<div id="active-directory-form-settings">
-				<form action="action.php?action=activedirectory_activedirectory_settings_save" method="POST">
-					<button type="submit" class="btn btn btn-success float-right"><i class="fas fa-check"></i> Enregistrer</button>
-					<legend>Configuration de l'AD</legend><br>
-					<div class="clear"></div>
-					<div class="row">
-						<div class="col-md-4">
-							<label>Serveur :</label>
-							<input type="text" name="ad-server" value="<?php echo $conf->get('plugin_activedirectory_server'); ?>" placeholder="ad.server.fr" class="form-control">
-						</div>
-						<div class="col-md-4">
-							<label>Port :</label>
-							<input type="text" name="ad-port" value="<?php echo $conf->get('plugin_activedirectory_port'); ?>" placeholder="389" class="form-control">
-						</div>
-						<div class="col-md-4">
-							<label>Port SSL :</label>
-							<input type="text" name="ad-ssl-port" value="<?php echo $conf->get('plugin_activedirectory_ssl_port'); ?>" placeholder="636" class="form-control">
-						</div>
-					</div><br>
-					<div class="row">
-						<div class="col-md-8">
-							<label>Racine des utilisateurs :</label>
-							<input type="text" name="ad-user-root" value="<?php echo $conf->get('plugin_activedirectory_user_root'); ?>" placeholder="ou=people,dc=idleman,dc=fr" class="form-control">
-						</div>
-					</div><br>
-					<div class="row">
-						<div class="col-md-8">
-							<label>Racine des groupes :</label>
-							<input type="text" name="ad-group-root" value="<?php echo $conf->get('plugin_activedirectory_group_root'); ?>" placeholder="ou=people,dc=idleman,dc=fr" class="form-control">
-						</div>
-						<div class="col-md-4">
-							<label>Domaine :</label>
-							<input type="text" name="ad-domain" value="<?php echo $conf->get('plugin_activedirectory_domain'); ?>" placeholder="@MONSITE.LOCAL" class="form-control">
-						</div>
-					</div><br>
-					<div class="row">
-						<div class="col-md-8">
-							<label>CN Lecture seule :</label>
-							<small class="text-muted">utilisé pour acceder aux groupes et aux utilisateurs</small>
-							<input type="text" name="ad-reader-login" value="<?php echo $conf->get('plugin_activedirectory_reader_login'); ?>" placeholder="ou=people,dc=idleman,dc=fr" class="form-control">
-						</div>
-
-						<div class="col-md-4">
-							<label>Mot de passe lecture seule :</label>
-							<small class="text-muted">(Optionnel)</small>
-							<input type="password" data-type="password" name="ad-reader-password" value="<?php echo $conf->get('plugin_activedirectory_reader_password'); ?>" placeholder="" class="form-control">
-						</div>
-					</div><br>
-					<div class="row">
-						<div class="col-md-8">
-							<label>CN Administrateur :</label>
-							<small class="text-muted">(Optionnel), utilisé pour les changements de mots de passe uniquement</small>
-							<input type="text" name="ad-admin-login" value="<?php echo $conf->get('plugin_activedirectory_admin_login'); ?>" placeholder="ou=people,dc=idleman,dc=fr" class="form-control">
-						</div>
-
-						<div class="col-md-4">
-							<label>Mot de passe Administrateur :</label>
-							<small class="text-muted">(Optionnel)</small>
-							<input type="password" data-type="password" name="ad-admin-password" value="<?php echo $conf->get('plugin_activedirectory_admin_password'); ?>" placeholder="" class="form-control">
-						</div>
-					</div><br>
+		<div class="clear"></div>
+		<hr>
+		<?php echo Configuration::html('activedirectory'); ?>
+		<hr>
+	</div>
 
-					<div class="row mt-3">
-						<!-- search results -->
-						<div class="col-xl-12">
-						 <h4>Meta informations</h4>
-						 <p>Vous pouvez remplir des méta champs pour les utilisateurs, ces méta champs sont requis par certains plugins et peuvent être renseigné
-						 depuis l'AD via la syntaxe : <code>Libellé:Type:nom-champ-ad:nom-meta</code> avec une ligne par champs</p>
-						 <textarea class="form-control" name="ad-metafields"><?php echo $conf->get('plugin_activedirectory_metafields'); ?></textarea>
-						</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>
-					
-				</form>
-				<hr/>
-				<form id="ad-correspondance" data-id="" class="" data-action="activedirectory_activedirectory_save_link" action="action.php?action=activedirectory_activedirectory_save_link" method="POST">
-					<legend>Correspondance Groupe / Rang / Établissement</legend><br>
-					<div class="row">
-						<div class="col-md-4">
-							<label>Nom du groupe AD :</label>
-							<input type="text" name="ad-group" id="ad-group" placeholder="Nom du groupe" class="form-control"> 
-						</div>
-						<div class="col-md-3">
-							<label>Rang :</label>
-							<select name="ad-rank" id="ad-rank" class="form-control">
-								<?php 
-								$ranks = new Rank();
-								$ranks = $ranks->populate();
-								foreach($ranks as $rank)
-									echo '<option value="'.$rank->getId().'">'.$rank->getLabel().'</option>';
-								?>
-							</select>
-						</div>
-						<div class="col-md-3">
-							<label>Établissement :</label>
-							<select name="ad-firm" id="ad-firm" class="form-control">
-								<?php 
-								$firms = new Firm();
-								$firms = $firms->populate();
-								foreach($firms as $firm)
-									echo '<option value="'.$firm->id.'">'.$firm->label.'</option>';
-								?>
-							</select>
-						</div>
-						<div class="col-md-2 position-relative text-right">
-							<div class="btn btn-success noLabel" onclick="activedirectory_activedirectory_save_link();"><i class="fas fa-check"></i> Valider</div>
-						</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>
-				</form>
-
-				<table id="group-rank-firm" class="table table-striped table-bordered table-hover table-group-rank-firm ">
-					<thead>
-						<tr>
-							<th>Groupe</th>
-							<th>Rang</th>
-							<th>Établissement</th>
-							<th></th>
-						</tr>
-					</thead>
-					<?php 
-					$rank_groups = ActiveDirectoryGroup::loadAll(array());
-					$firms = Firm::loadAll(array());
-
-					$rankManager = new Rank();
-					$rankManager = $rankManager->populate();
-					$rankAnnuary = array();
-
-					foreach($rankManager as $rank)
-						$rankAnnuary[$rank->getId()] = $rank->getLabel();
-
-					foreach($rank_groups as $rank_group){
-						?>
-						<tr data-id="<?php echo $rank_group->id; ?>">
-							<td><?php echo $rank_group->adgroup ?></td>
-							<td><?php echo $rankAnnuary[$rank_group->rank] ?></td>
-							<td><?php echo Firm::load(array('id'=>$rank_group->firm))->label ?></td>
-							<td class="text-center action-buttons">
-								<div class="btn btn-info btn-squarred btn-mini" onclick="activedirectory_activedirectory_edit_link(this);"><i class="fas fa-pencil-alt"></i></div>
-								<a class="btn btn-danger btn-squarred btn-mini" onclick="return confirm('Voulez-vous vraiment supprimer cet élément ?')" href="action.php?action=activedirectory_activedirectory_delete_link&groupId=<?php echo urlencode($rank_group->id); ?>"><i class="fas fa-times"></i></a>
-							</td>
-						</tr>
-					<?php } ?>
-				</table>
+				</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>
-		<?php } else { ?>
-		<div id="main" class="wrapper clearfix">
-			<article>
-					<h3>Vous devez être connecté</h3>
-			</article>
 		</div>
-		<?php } ?>
+		<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>
 

+ 1 - 1
plugin/customiser/app.json

@@ -2,7 +2,7 @@
 	"id": "fr.idleman.customiser",
 	"name": "Customiser",
 	"author" : {
-		"name" : "Administrateur SYS1",
+		"name" : "Administrateur ",
 		"mail" : "developpement@idleman.fr"
 	},
 	"version": "1.0",

+ 1 - 1
plugin/customiser/theme/example/app.json

@@ -3,7 +3,7 @@
 	"label": "Example",
 	"cover": "cover.jpg",
 	"author" : {
-		"name" : "Administrateur SYS1",
+		"name" : "Administrateur ",
 		"mail" : "developpement@idleman.fr"
 	}
 }

+ 1 - 1
plugin/example/js/main.js

@@ -56,7 +56,7 @@ function contact_search(callback, exportMode){
 					transform:'translateX(0px)',
 					opacity : 1
 				})
-			},(i+1)*20);
+			},(i+1)*10);
 		}
 	},function(response){
 		$('.results-count span').text(response.pagination.total);

+ 1 - 1
plugin/git/GitRepository.class.php

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

+ 1 - 1
plugin/git/app.json

@@ -2,7 +2,7 @@
 	"id": "fr.idleman.git",
 	"name": "Git",
 	"author" : {
-		"name" : "Administrateur SYS1",
+		"name" : "Administrateur ",
 		"mail" : "{{user.mail}}"
 	},
 	"version": "1.0",

+ 58 - 0
plugin/issue/IssueEvent.class.php

@@ -0,0 +1,58 @@
+<?php
+/**
+ * Define a issueevent.
+ * @author Administrateur
+ * @category Plugin
+ * @license copyright
+ */
+class IssueEvent extends Entity{
+
+	public $id;
+	public $type; //Type (Texte)
+	public $content; //Contenu (Texte Long)
+	public $issue; //Issue (Entier)
+
+	const TYPE_COMMENT = 'comment';
+	const TYPE_STATE = 'state';
+	const TYPE_ASSIGNATION = 'assignation';
+	const TYPE_TAG = 'tag';
+
+	protected $TABLE_NAME = 'issue_issue_event';
+	public $fields =
+	array(
+		'id' => 'key',
+		'type' => 'string',
+		'content' => 'longstring',
+		'issue' => 'int'
+	);
+
+	public $links = array(
+		'issue' => 'IssueReport'
+	);
+
+	public static function types($key=null){
+		$types = array(
+			TYPE_COMMENT => array('label' => 'Commentaire','icon' => 'fas fa-comment','color' => '#cecece'),
+			TYPE_STATE => array('label' => 'Changement d\'état','icon' => 'fas fa-comment','color' => '#cecece'),
+			TYPE_ASSIGNATION => array('label' => 'Nouvelle assignation','icon' => 'fas fa-comment','color' => '#cecece'),
+			TYPE_TAG => array('label' => 'Modification tags','icon' => 'fas fa-comment','color' => '#cecece'),
+		);
+		$default =  array('label' => ' - ','icon' => 'fas fa-comment','color' => '#cecece');
+		if(!isset($key)) return $types;
+
+		return isset($types[$key]) ? $types[$key] : $default;
+	}
+
+	public function dir($relativePath = false){
+		return ($relativePath ? '' : File::dir()).'issue'.SLASH.'attachments'.SLASH.$this->id;
+	}
+
+	public function remove(){
+		self::deleteById($this->id);
+
+		//supression des pieces jointes
+		if(file_exists($this->dir())) delete_folder_tree($this->dir(),true);
+
+	}
+}
+?>

+ 49 - 0
plugin/issue/IssueReport.class.php

@@ -0,0 +1,49 @@
+<?php
+/**
+ * Define a issuereport.
+ * @author Administrateur
+ * @category Plugin
+ * @license copyright
+ */
+class IssueReport extends Entity{
+	public $id,$comment,$browser,$browserVersion,$online,$os,$from,$width,$height,$ip,$history,$state,$assign;
+	protected $TABLE_NAME = 'issue_report';
+	public $fields =
+	array(
+		'id' => 'key',
+		'browser' => 'string',
+		'browserVersion' => 'string',
+		'online' => 'boolean',
+		'state' => 'string',
+		'assign' => 'string',
+		'os' => 'string',
+		'from' => 'string',
+		'width' => 'int',
+		'height' => 'int',
+		'history' => 'longstring',
+		'ip' => 'string'
+	);
+
+	public static function states($key=null){
+		$states = array(
+			'open' => array('icon'=>'far fa-clock','label'=>'Ouvert','color'=>'#3c3c3c'),
+			'closed' => array('icon'=>'fas fa-check','label'=>'Résolu','color'=>'#2cbe4e'),
+			'canceled' => array('icon'=>'fas fa-ban','label'=>'Annulé','color'=>'#cb2431'),
+		);
+
+		if(!isset($key)) return $states;
+		return isset($states[$key]) ? $states[$key] : array(); 
+	}
+
+
+	public function remove($cascading = true){
+		self::deleteById($this->id);
+		if($cascading){
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+			foreach(IssueEvent::loadAll(array('issue'=>$this->id)) as $event){
+				$event->remove();
+			}
+		}
+	}
+}
+?>

+ 30 - 0
plugin/issue/IssueReportTag.class.php

@@ -0,0 +1,30 @@
+<?php
+/**
+ * Define a IssueReportTag.
+ * @author Administrateur 
+ * @category Plugin
+ * @license copyright
+ */
+class IssueReportTag extends Entity{
+	public $id,$tag,$report;
+	protected $TABLE_NAME = 'issue_report_tag';
+	public $fields =
+	array(
+		'id' => 'key',
+		'tag' => 'string',
+		'report' => 'int'
+	);
+
+
+	public static function tags($key=null){
+		$tags = array(
+			'bug' => array('icon'=>'fas fa-bug','label'=>'Bug','color'=>'#cb2431'),
+			'feature' => array('icon'=>'fas fa-check','label'=>'Demande','color'=>'#2cbe4e'),
+			'question' => array('icon'=>'far fa-question-circle','label'=>'Question','color'=>'#00BCD4')
+		);
+
+		if(!isset($key)) return $tags;
+		return isset($tags[$key]) ? $tags[$key] : array(); 
+	}
+}
+?>

+ 519 - 0
plugin/issue/action.php

@@ -0,0 +1,519 @@
+<?php
+global $_,$conf;
+switch($_['action']){
+	/** ISSUEREPORT **/
+	//Récuperation d'une liste de issuereport
+	case 'issue_issuereport_search':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('issue','read');
+			require_once(__DIR__.SLASH.'IssueReport.class.php');
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+			require_once(__DIR__.SLASH.'IssueReportTag.class.php');
+
+			$allTags = IssueReportTag::tags();
+			$query = 'SELECT DISTINCT {{table}}.id, {{table}}.*,(SELECT ie.content FROM '.IssueEvent::tableName().' ie WHERE ie.type=? AND ie.issue={{table}}.id ORDER BY id ASC LIMIT 1) as comment,(SELECT COUNT(ie2.id) FROM '.IssueEvent::tableName().' ie2 WHERE ie2.type=? AND ie2.issue={{table}}.id) as comments FROM {{table}} LEFT JOIN '.IssueReportTag::tableName().' t ON t.report={{table}}.id WHERE 1';
+			$data = array(IssueEvent::TYPE_COMMENT,IssueEvent::TYPE_COMMENT);
+
+			//Recherche simple
+			if(!empty($_['filters']['keyword'])){
+				$query .= ' AND {{table}}.label LIKE ?';
+				$data[] = '%'.$_['filters']['keyword'].'%';
+			}
+
+			$tags = array_filter(explode(',',$_['tags']));
+			if(count($tags)!=0){
+				$query .= ' AND t.tag IN ("'.implode('","',$tags).'")';
+			}
+
+			//Recherche avancée
+			if(isset($_['filters']['advanced'])) filter_secure_query($_['filters']['advanced'],array('from','comment','{{table}}.creator','{{table}}.created','browser','ip','state'),$query,$data);
+			$query .= ' ORDER BY {{table}}.id desc ';
+			
+			//Pagination
+			$response['pagination'] = IssueReport::paginate(20,(!empty($_['page'])?$_['page']:0),$query,$data);
+
+			foreach(IssueReport::staticQuery($query,$data) as $row){
+				//$row = $issueReport->toArray(true);
+				
+
+				
+				$row['state'] = IssueReport::states($row['state']);
+				$row['relativefrom'] = str_replace(ROOT_URL,'',$row['from']);
+				$row['date'] = date('d-m-Y',$row['created']);
+				$row['hour'] = date('H:i',$row['created']);
+				$row['osIcon'] = 'fas fa-question-circle';
+				$row['browserIcon'] = 'fas fa-question-circle';
+
+				$row['comments'] = $row['comments']==1 ? false: $row['comments'];
+
+				$row['excerpt'] = isset($row['comment']) ? truncate(strip_tags($row['comment']),150) : 'Aucun commentaire';
+
+				if(strlen($row['os'])>=3 && substr(strtolower($row['os']),0,3) == 'win') $row['osIcon'] = 'fab fa-windows text-primary';
+				if(strlen($row['os'])>=5 && substr(strtolower($row['os']),0,5) == 'linux') $row['osIcon'] = 'fab fa-linux text-danger';
+				if(strlen($row['os'])>=3 && substr(strtolower($row['os']),0,3) == 'mac') $row['osIcon'] = 'fab fa-apple text-secondary';
+				
+				switch($row['browser']){
+					case 'firefox': $row['browserIcon'] = 'fab fa-firefox text-warning'; break;
+					case 'ie': $row['browserIcon'] = 'fab fa-internet-explorer text-danger'; break;
+					case 'edge': $row['browserIcon'] = 'fab fa-edge text-primary'; break;
+					case 'chrome': $row['browserIcon'] = 'fab fa-chrome text-success'; break;
+				}
+
+				$row['tags'] = array();
+				foreach(IssueReportTag::loadAll(array('report'=>$row['id'])) as $tag){
+					if(!isset($allTags[$tag->tag])) continue;
+					$row['tags'][] = $allTags[$tag->tag];
+				}
+				$response['rows'][] = $row;
+			}
+		});
+	break;
+
+	case 'issue_add_document':
+	Action::write(function(&$response){
+		global $myUser,$_;
+		if(!$myUser->connected()) throw new Exception("Vous devez être connecté",401);
+		require_once(__DIR__.SLASH.'IssueReport.class.php');
+		require_once(__DIR__.SLASH.'IssueEvent.class.php');
+
+		$report = IssueReport::provide();
+		$report->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'],'issue'.SLASH.'screens'.SLASH.$report->id.SLASH.$name);
+			$row['url'] = 'action.php?action=issue_download_document&event='.$event->id.'&path='.base64_encode($file['name']);
+			$row['oldPath'] = $file['path'];
+			$response['files'][] = $row;
+		}
+		$response['id'] = $report->id;
+	});
+	break;
+
+	//Téléchargement des documents
+	case 'issue_download_document':
+		global $myUser,$_;
+		if(!$myUser->connected()) throw new Exception("Vous devez être connecté",401);
+
+		require_once(__DIR__.SLASH.'IssueEvent.class.php');
+
+		$event = IssueEvent::getById($_['event']);
+		$path = str_replace(array('..','/','\\'),'',base64_decode($_['path']));
+		$path = $event->dir().SLASH.$path;
+		File::downloadFile($path);
+	break;
+
+	case 'issue_issuereport_meta_save':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			require_once(__DIR__.SLASH.'IssueReport.class.php');
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+			require_once(__DIR__.SLASH.'IssueReportTag.class.php');
+			if(!array_key_exists($_['state'], IssueReport::states())) throw new Exception("L'état du ticket est invalide");
+
+			User::check_access('issue','edit');
+
+			$item = IssueReport::provide();
+			$oldItem = clone $item; 
+
+			$item->state = $_['state'];
+			if(!empty($_['assign'])) $item->assign = $_['assign'];
+			$item->save();
+
+
+			//Maj des tags
+			$existingTags = IssueReportTag::loadAll(array('report'=>$item->id));
+			$tags = explode(',',$_['tags']);
+
+			$tagsAction = false;
+			$similarTags = array();
+			foreach ($existingTags as $existingTag) {
+				if(in_array($existingTag->tag, $tags)){
+					$similarTags[] = $existingTag->tag;
+					continue;
+				}
+				IssueReportTag::deleteById($existingTag->id);
+				//var_dump($existingTag->tag);
+				$tagsAction = array('action'=>'deleted','tag'=>IssueReportTag::tags($existingTag->tag));
+			}
+			foreach ($tags as $tag) {
+				if(in_array($tag, $similarTags)) continue;
+				$newTag = new IssueReportTag();
+				$newTag->tag = $tag;
+				$newTag->report = $item->id;
+				$newTag->save();
+				$tagsAction = array('action'=>'added','tag'=>IssueReportTag::tags($tag));
+			}
+			
+			if($tagsAction!=false){
+				$event = new IssueEvent();
+				$event->type = IssueEvent::TYPE_TAG;
+				$event->issue = $item->id;
+				$event->content = json_encode($tagsAction);
+				$event->save();
+			}
+			
+			
+			//Si l'assignation a changée on envoi une notification
+			if($oldItem->assign != $item->assign){
+
+				$event = new IssueEvent();
+				$event->type = IssueEvent::TYPE_ASSIGNATION;
+				$event->issue = $item->id;
+				$event->content = json_encode(array(
+					'assigned' => $item->assign
+				));
+				$event->save();
+
+				if($item->assign != $myUser->login){
+					Plugin::callHook("emit_notification", array(array(
+						'label' => '['.PROGRAM_NAME.' - '.PROGRAM_UID.'] Le Ticket #'.$item->id.' vous a été assigné',
+						'html' => "Le ticket <a href='".ROOT_URL."/index.php?module=issue&page=sheet.report&id=".$item->id."'>#".$item->id."</a> vous a été assigné par ".(!empty($myUser->fullName())?$myUser->fullName():$myUser->login)." le ".date('d-m-Y à H:i').".
+						<br>Bonne chance :).",
+						'type' => "issue",
+						'meta' => array('link' => ROOT_URL.'/index.php?module=issue&page=sheet.report&id='.$item->id),
+						'recipients' => array($item->assign)
+					)));
+				}
+			}
+
+			//Si l'etat a changé on envois une notification
+			if($oldItem->state != $item->state){
+				switch ($item->state) {
+					case 'closed':
+						$infos = array(
+							'label' => "Votre Ticket #".$item->id." est résolu",
+							'html' => "Le ticket d'erreur <a href='".ROOT_URL."/index.php?module=issue&page=sheet.report&id=".$item->id."'>#".$item->id."</a> a été marqué comme résolu par ".(!empty($myUser->fullName())?$myUser->fullName():$myUser->login).' le '.date('d-m-Y à H:i').'.
+							<br>Il est possible que la résolution de cette erreur ne soit mise en production que dans quelques jours.',
+							'type' => "notice",
+							'meta' => array('link' => ROOT_URL.'/index.php?module=issue&page=sheet.report&id='.$item->id),
+							'recipients' => array($item->creator)
+						);
+					break;
+					case 'open':
+						$infos = array(
+							'label' => "Votre Ticket #".$item->id." a été ré-ouvert",
+							'html' => "Le ticket d'erreur <a href='".ROOT_URL."/index.php?module=issue&page=sheet.report&id=".$item->id."'>#".$item->id."</a> a été ré-ouvert par ".(!empty($myUser->fullName())?$myUser->fullName():$myUser->login).' le '.date('d-m-Y à H:i').'.
+							<br>Il est possible que la résolution de cette erreur ne soit mise en production que dans quelques jours.',
+							'type' => "notice",
+							'meta' => array('link' => ROOT_URL.'/index.php?module=issue&page=sheet.report&id='.$item->id),
+							'recipients' => array($item->creator)
+						);
+					break;
+					case 'canceled':
+						$infos = array(
+							'label' => "Votre Ticket #".$item->id." a été annulé",
+							'html' => "Le ticket d'erreur <a href='".ROOT_URL."/index.php?module=issue&page=sheet.report&id=".$item->id."'>#".$item->id."</a> a été annulé ".(!empty($myUser->fullName())?$myUser->fullName():$myUser->login).' le '.date('d-m-Y à H:i').'.
+							<br>Il est possible que ce ticket soit un doublon ou que son contenu soit innaproprié.',
+							'type' => "notice",
+							'meta' => array('link' => ROOT_URL.'/index.php?module=issue&page=sheet.report&id='.$item->id),
+							'recipients' => array($item->creator)
+						);
+					break;
+					default:
+						# code...
+					break;
+				}
+				$event = new IssueEvent();
+				$event->type = IssueEvent::TYPE_STATE;
+				$event->issue = $item->id;
+				$event->content = json_encode(array(
+					'old' => $oldItem->state,
+					'new' => $item->state
+				));
+				$event->save();
+				Plugin::callHook("emit_notification", array($infos));
+			}
+
+			
+		});
+	break;
+	
+	//Ajout ou modification d'élément issuereport
+	case 'issue_issuereport_save':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			require_once(__DIR__.SLASH.'IssueReport.class.php');
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+			require_once(__DIR__.SLASH.'IssueReportTag.class.php');
+			if(!$myUser->connected()) throw new Exception("Vous devez être connecté pour laisser un rapport d'erreur",401);
+			$item = IssueReport::provide();
+			if(!$myUser->can('issue','edit') && is_numeric($item->id) &&  $item->id!=0) throw new Exception("Permissions insuffisantes",403);
+			
+			$tags = array_filter(explode(',',$_['tags']));
+			if(count($tags)==0) throw new Exception("Merci de sélectionner au moins une catégorie");
+			if(!isset($_['issue-comment']) || empty($_['issue-comment'])) throw new Exception("Merci de commenter votre ticket");
+
+			
+			$item->browser = $_['browser'];
+			$item->browserVersion = $_['browserVersion'];
+			$item->online = $_['online'];
+			$item->os = $_['os'];
+			$item->state = 'open';
+			$item->from = $_['from'];
+			$item->width = $_['width'];
+			$item->height = $_['height'];
+			$item->history = $_['history'];
+			$item->ip = ip();
+			$item->save();
+
+
+			$firstEvent = new IssueEvent();
+			$firstEvent->created =  $item->created;
+			$firstEvent->updated =  $item->updated;
+			$firstEvent->creator =  $item->creator;
+			$firstEvent->updater =  $item->updater;
+			$firstEvent->type =  IssueEvent::TYPE_COMMENT;
+			$firstEvent->content =   $_['issue-comment'];
+			$firstEvent->issue =  $item->id;
+			$firstEvent->save();
+
+			$response['item'] = $item->toArray();
+			
+			IssueReportTag::delete(array('report'=>$item->id));
+			foreach ($tags as $tag) {
+				$reportTag = new IssueReportTag();
+				$reportTag->tag = $tag;
+				$reportTag->report = $item->id;
+				$reportTag->save();
+			}
+			
+			if(!empty($_['screenshot'])){
+				$screenshot = str_replace('data:image/png;base64,', '', $_['screenshot']);
+				$screenshot = str_replace(' ', '+', $screenshot);
+				$stream = base64_decode($screenshot);
+				
+				$attachmentFolder = $firstEvent->dir();
+				if(!file_exists($attachmentFolder)) mkdir($attachmentFolder,0755,true);
+				$screenPath = $attachmentFolder.SLASH.'screenshot.jpg';
+				file_put_contents($screenPath, $stream);
+			}
+
+			//Ajout des fichiers joints
+			if(!empty($_['document_temporary'])){
+				$files = json_decode($_['document_temporary'],true);
+
+				$attachmentFolderRelative =  $firstEvent->dir(true);
+				$attachmentFolder =  $firstEvent->dir();
+				if(!file_exists($attachmentFolder)) mkdir($attachmentFolder,0755,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'];
+					File::move($from, $attachmentFolderRelative.SLASH.$to);
+				}
+			}
+
+			$data = $item->toArray();
+			$data['comment'] = html_entity_decode($firstEvent->content);
+			$data['url'] = ROOT_URL.'/index.php?module=issue&page=sheet.report&id='.$data['id'];
+			
+			if(!empty($conf->get('issue_report_mails'))){
+				
+
+				$path = __DIR__.SLASH.'mail.template.php';
+				if(!file_exists($path)) return;
+				$stream = file_get_contents($path);
+
+				$recipients = array();
+				foreach (explode(',', $conf->get('issue_report_mails')) as $recipient) {
+					if(is_numeric($recipient)){
+						foreach(UserFirmRank::loadAll(array('rank'=>$recipient)) as $ufr)
+							$recipients[] = $ufr->user;
+						continue;
+					}
+					$recipients[] = $recipient;
+				}
+				// Émission d'une notification pour les devs
+				Plugin::callHook("emit_notification", array(array(
+					'label' => '['.PROGRAM_NAME.' - '.PROGRAM_UID.'] Ticket #'.$item->id.' ouvert',
+					'html' => template($stream,$data),
+					'type' => "issue",
+					'meta' => array('link' => $data['url']),
+					'recipients' => $recipients
+				)));
+			}
+
+			// Émission d'une notification pour l'auteur du ticket
+			Plugin::callHook("emit_notification", array(array(
+				'label' => "Votre Ticket #".$item->id." a été envoyé",
+				'html' => "Le ticket <a href='".ROOT_URL."/index.php?module=issue&page=sheet.report&id=".$item->id."'>#".$item->id."</a> a été créé et sera pris en compte par nos techniciens, vous pouvez consulter
+				son avancement en <a href='".ROOT_URL."/index.php?module=issue&page=sheet.report&id=".$item->id."'>cliquant ici</a>",
+				'type' => "notice",
+				'meta' => array('link' => $data['url']),
+				'recipients' => array($item->creator)
+			)));
+
+			Log::put('Déclaration d\'un rapport d\'erreur #'.$item->toText(), 'Issue');
+		});
+	break;
+	
+	case 'issue_screenshot_download':
+		global $myUser,$_;
+		if(!$myUser->connected()) throw new Exception("Vous devez être connecté",401);
+		try {
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+			$event = IssueEvent::getById($_['id']);
+			File::downloadFile($event->dir().SLASH.'screenshot.jpg');
+		} catch(Exception $e) {
+			File::downloadFile(__ROOT__.'img/default-image.png');
+		}
+	break;
+
+	//Suppression d'élement issuereport
+	case 'issue_issuereport_delete':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('issue','configure');
+			require_once(__DIR__.SLASH.'IssueReport.class.php');
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+			
+			$issue = IssueReport::provide();
+			$issue->remove();
+			
+		});
+	break;
+	
+	//Sauvegarde des configurations de issue
+	case 'issue_setting_save':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('issue','configure');
+			foreach(Configuration::setting('issue') as $key=>$value){
+				if(!is_array($value)) continue;
+				$allowed[] = $key;
+			}
+			foreach ($_['fields'] as $key => $value)
+				if(in_array($key, $allowed)) $conf->put($key,$value);
+		});
+	break;
+
+
+	/** EVENT **/
+	//Récuperation d'une liste de issueevent
+	case 'issue_issue_event_search':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('issue','read');
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+			require_once(__DIR__.SLASH.'IssueReport.class.php');
+
+			
+			
+			$events = IssueEvent::loadAll(array('issue'=>$_['issue']),array('id ASC'));
+			
+			foreach($events as $i=>$event){
+
+				$row = $event->toArray();
+				$creator = User::byLogin($event->creator);
+				$row['creator'] = $creator->toArray();
+				$row['avatar'] = $creator->getAvatar(); 
+				$row['fullName'] = $creator->fullName(); 
+				$row['createdRelative'] = relative_time($event->created);
+
+				switch($event->type){
+					case 'comment':
+
+						$row['classes'] = $i == 0 ? 'issue-first-comment' : '';
+						$row['files'] = array();
+						foreach (glob($event->dir().SLASH.'*') as $key => $value){
+							$file = array();
+							$file['extension'] = getExt($value);
+							$file['label'] = mt_basename($value);
+							$file['labelExcerpt'] = truncate($file['label'],35);
+							$file['type'] = in_array($file['extension'], array('jpg','png','jpeg','bmp','gif','svg')) ? 'image' : 'file';
+							$file['url'] = 'action.php?action=issue_download_document&event='.$event->id.'&path='.base64_encode(basename($value)); 
+							$file['icon'] = getExtIcon($file['extension']);
+							$row['files'][] = $file;
+						}
+
+						if($creator->login == $myUser->login || $myUser->can('issue','configure'))  $row['classes'] .=' editable';
+						$row['comment'] =  empty($event->content) ? 'Pas de commentaires': html_entity_decode($event->content); 
+						$row['hasfiles'] = count($row['files']) > 0 ? true : false;
+
+					break;
+					case IssueEvent::TYPE_STATE:
+						$infos = json_decode($row['content'],true);
+						$row['oldstate'] = IssueReport::states($infos['old']);
+						$row['state'] = IssueReport::states($infos['new']);
+					break;
+					case IssueEvent::TYPE_TAG:
+						$infos = json_decode($row['content'],true);
+						
+						$row['action'] = $infos['action'] == 'added' ? 'ajouté' :'supprimé';
+						$row['tag'] = $infos['tag'];
+					break;
+					case IssueEvent::TYPE_ASSIGNATION:
+						$infos = json_decode($row['content'],true);
+						$assigned = User::byLogin($infos['assigned']);
+						$row['assigned'] = array(
+							'fullName' => $assigned->fullName(),
+							'avatar' => $assigned->getAvatar()
+						);
+					break;
+				}
+
+				
+				
+				$response['rows'][] = $row;
+			}
+
+			
+
+		});
+	break;
+	
+	//Ajout ou modification d'élément issueevent
+	case 'issue_issue_event_save':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('issue','edit');
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+			$item = IssueEvent::provide();
+			$item->type = IssueEvent::TYPE_COMMENT;
+			$item->content = $_['content'];
+			$item->issue = $_['issue'];
+			$item->save();
+		});
+	break;
+	
+	//Récuperation ou edition d'élément issueevent
+	case 'issue_issue_event_edit':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('issue','edit');
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+			require_once(__DIR__.SLASH.'IssueReport.class.php');
+			$response = IssueEvent::getById($_['id'],1);
+		});
+	break;
+
+	//Suppression d'élement issueevent
+	case 'issue_issue_event_delete':
+		Action::write(function(&$response){
+			global $myUser,$_;
+			User::check_access('issue','delete');
+			require_once(__DIR__.SLASH.'IssueReport.class.php');
+			require_once(__DIR__.SLASH.'IssueEvent.class.php');
+
+			$event = IssueEvent::getById($_['id'],1);
+			$issue = $event->join('issue');
+
+			if($issue->creator != $myUser->login && !$myUser->can('issue','configure')) throw new Exception("Permission refusée");
+			
+
+			$event->remove();
+
+			if(IssueEvent::rowCount(array('issue'=>$issue->id)) == 0){
+				$issue->remove(false);
+				$response['redirect'] =  'setting.php?section=global.report';
+			}
+			
+			
+		});
+	break;
+
+
+}
+?>

+ 13 - 0
plugin/issue/app.json

@@ -0,0 +1,13 @@
+{
+	"id": "fr.idleman.issue",
+	"name": "Issue",
+	"author" : {
+		"name" : "Administrateur SYS1",
+		"mail" : "developpement@idleman.fr"
+	},
+	"version": "1.0",
+	"url": "http://idleman.fr",
+	"licence": {"name": "Copyright","url" : ""},
+	"description": "Permet le rapport de bugs sur le logiciel courant",
+	"require" : {}
+}

+ 24 - 0
plugin/issue/css/component.css

@@ -0,0 +1,24 @@
+.data-type-tagcloud{
+	margin: 0;
+	padding: 0;
+}
+.data-type-tagcloud li{
+	display: inline-block;
+	vertical-align: top;
+	margin: 0;
+	box-sizing: border-box;
+	padding: 2px;
+}
+.badge.badge-tag{
+	border-radius: 50px;
+	padding: 4px 6px 4px 6px;
+    border: 1px dashed #cecece;
+    cursor: pointer;
+    color: #c1c1c1;
+    font-weight: normal;
+	font-size: 0.75em;
+    transition: all 0.2s ease-in-out;
+}
+.badge.badge-tag:hover{
+    border-color: #ffc107;
+}

+ 375 - 0
plugin/issue/css/main.css

@@ -0,0 +1,375 @@
+/** ISSUE **/
+.issue-report-modal {
+	z-index: 1110;
+}
+.issue-report-modal .issue-screenshot{
+	overflow: auto;
+	margin-top: 5px;
+	max-height: 300px;
+	border: 5px dotted #cecece;
+}
+.issue-report-modal .issue-screenshot img{
+	width: 100%;
+	height: auto;
+	padding: 5px;
+}
+.issue-report-modal .badge.badge-tag {
+	border-radius: 50px;
+	font-size: 15px;
+	padding: 4px 6px 4px 6px;
+	border: 1px dashed #007bff;
+	cursor: pointer;
+	color: #777777;
+	font-weight: normal;
+	transition: all 0.2s ease-in-out;
+}
+.issue-report-modal span.page-link-issue {
+	word-break: break-all;
+	white-space: pre-wrap;
+	text-align: left;
+	font-size: 0.9em;
+}
+.issue-report-modal .modal-dialog{
+    max-width: 99% !important;
+}
+.issue-report-modal .issue-form .trumbowyg-editor-visible {
+	margin-top: 0;
+}
+.issue-report-modal .issue-form textarea,
+.issue-report-modal .issue-form .trumbowyg-editor {
+	height: 250px;
+	min-height: 250px;
+}
+
+
+
+.issue-declare-button{
+	cursor: pointer;
+	position: fixed;
+	bottom: 10px;
+	left: 10px;
+	opacity: 0.5;
+	border-radius: 100%;
+	z-index: 1050;
+	width: 30px;
+	height: 30px;
+	text-align: center;
+	padding-top: 3px;
+	box-sizing: border-box; 
+	background-color: #cecece;
+	transition: all 0.1s linear;
+}
+.issue-declare-button:hover{
+	opacity: 0.7;
+	transform: scale(1.1);
+}
+.list-issue-reports .created-head,
+.list-issue-reports .created-cell {
+	width: 130px;
+    text-align: center;
+}
+#issuereports .button-head {
+	width: 180px;
+}
+#issuereports .button-cell {
+	padding: 0;
+	vertical-align: middle;
+	text-align: center;
+}
+#issuereports .infos-cell {
+	padding: 0.25rem .25rem;
+	vertical-align: middle;
+	position: relative;
+}
+
+
+#issuereports > li .report-buttons{
+	font-size: 20px;
+    position: absolute;
+    right: 50px;
+    top: 50%;
+}
+#issuereports > li .report-buttons .btn-delete{
+    opacity: 0.5;
+    font-size: 16px;
+    margin-left:10px;
+    transition: opacity 0.2s ease-in-out;
+}
+
+
+#issuereports > li .report-buttons .btn-delete:hover{
+	opacity:1;
+}
+
+#issuereports{
+	margin:0;
+	padding: 0;
+}
+
+#issuereports > li,.issue-reports-search{
+    background-color: #ffffff;
+    border-radius: 3px;
+    list-style-type: none;
+    padding: 15px;
+}
+
+#issuereports > li{
+    margin: 0 0 15px 0;
+}
+
+#issuereports > li .issue-state{
+	margin-top: 11px;
+    font-size: 30px;
+    display: inline-block;
+    width: 50px;
+    vertical-align: top;
+}
+#issuereports > li .infos-cell{
+	display: inline-block;
+	vertical-align: top;
+}
+#issuereports .issue-state{
+	width:40px;
+	vertical-align: middle;
+    text-align: center;
+    font-size: 20px;
+}
+
+#issuereports .issue-title{
+	color:#333;
+}
+#issuereports .assign{
+	color:#555;
+	background: #f4f4f4;
+	font-style: italic;
+	font-size: 10px;
+	padding:3px;
+	display: inline-block;
+	margin-top: 3px;
+	vertical-align: top;
+}
+
+.section-global-report .nav-link{
+	border-radius: 3px;
+	border:0;
+}
+.section-global-report .nav-tabs {
+    border:0;
+}
+
+.section-global-report .data-type-tagcloud {
+	display: inline-flex;
+}
+div.issue-state-list > .dropdown-menu {
+	padding: 0;
+}
+div.issue-state-list > .dropdown-menu > a:hover {
+	opacity: 0.9;
+	transition: all 0.05s ease-in-out;
+}
+#issuereports .report-tag,#issuereports .report-tag li{
+	display: inline-flex;
+	vertical-align: top;
+	margin: 0 1px;
+	padding: 0;
+}
+#issuereports .report-tag li .badge.badge-tag{
+	font-size: 10px;
+	padding: 3px;
+	cursor: default;
+}
+#issuereports .text-muted{
+	color:#cecece;
+	font-size: 12px;
+}
+#issuereport-form .report-tag,
+#issuereport-form .report-tag li {
+    display: inline-flex;
+    vertical-align: middle;
+    padding: 0;
+}
+#issuereport-form .report-tag{
+	margin-left: 10px;
+}
+.issuereport-form .page-container a {
+	word-break: break-all;
+	font-size: 0.7em;
+}
+.issue-form {
+	margin-bottom: 20px;
+}
+
+@keyframes spinner {
+  to {transform: rotate(360deg);}
+}
+ 
+.rotate {
+  animation: spinner 1s linear infinite;
+}
+
+
+.module-issue.page-sheet-report, 
+.module-issue.page-sheet-report body,
+.section-global-report body{
+	background:#f3f3f3;
+}
+
+
+.module-issue .issue-events{
+	padding:0;
+	margin: 0;
+	list-style-type:none;
+}
+
+.module-issue .issue-events .issue-event,.module-issue .issue-event-form{
+	background-color: #ffffff;
+	border-radius: 3px;
+	list-style-type:none;
+	padding:15px;
+	margin:0 0 15px 0;
+}
+.module-issue .issue-events .issue-event.issue-state,
+.module-issue .issue-events .issue-event.issue-assignation,
+.module-issue .issue-events .issue-event.issue-tag{
+	background-color: transparent;
+	padding:10px 15px;
+}
+
+.module-issue .issue-event-form .trumbowyg-box, 
+.module-issue .issue-event-form .trumbowyg-editor{
+	border:0;
+}
+
+.module-issue .issue-event-form .trumbowyg-box, 
+.module-issue .issue-event-form .trumbowyg-editor { 
+	min-height: 150px; 
+} 
+
+
+.module-issue  .issue-event  .only-editable{
+	display: none;
+	margin:0;
+}
+.module-issue  .issue-event  .only-editable > li{
+	cursor: pointer;
+	font-size:14px;
+	display: inline-block;
+	list-style-type:none;
+	padding:3px;
+	opacity:0.5;
+	margin:0;
+	transition: opacity 0.2s ease-in-out;
+}
+.module-issue  .issue-event  .only-editable > li:hover{
+	opacity:1;
+}
+
+.module-issue  .issue-event.editable  .only-editable{
+	display: inline-block;
+}
+
+
+
+
+.module-issue .issue-sidebar .list-group-item{
+	border:0;
+}
+
+.module-issue .issue-event .comment-header{
+	width: 100%;
+	padding-bottom: 10px;
+	border-bottom:1px solid #f8f8f8;
+}
+
+.module-issue .data-type-user{
+	border:0;
+}
+
+.module-issue .issue-event .header-infos{
+	display: inline-block;
+	margin-left: 8px;
+	margin-top: 5px;
+	vertical-align: top;
+}
+
+.module-issue .issue-event .comment-message{
+	margin-top: 10px;
+}
+
+.module-issue .comment-attachments > h5{
+	font-size: 14px;
+	font-weight: bold;
+	color:#9da9b3;
+}
+.module-issue .comment-attachment .attachment-image{
+	overflow :hidden;
+	display: inline-block;
+}
+.module-issue .comment-attachment.attachment-image img{
+	max-height: 150px;
+	width: auto;
+}
+
+
+.module-issue .issue-sidebar h5{
+	font-size: 14px;
+	font-weight: bold;
+	color:#9da9b3;
+}
+
+
+.module-issue .issue-event-form [data-type="dropzone"]{
+	height:50px;
+	border-radius: 3px;
+    border: 0;
+}
+
+.module-issue .issue-event-form [data-type="dropzone"] > div {
+    font-size: 11px;
+    color: #999999;
+    text-align: center;
+    padding: 18px 5px 5px 5px;
+    text-transform: uppercase;
+    font-weight: bold;
+}
+
+.module-issue .comment-attachment{
+	opacity: 0.8;
+	transition: opacity 0.2s ease-in-out;
+	display: inline-block;
+	vertical-align: top;
+	color: inherit;
+	text-decoration: none;
+}
+.module-issue .comment-attachment:hover{
+	opacity: 1;
+}
+.module-issue .comment-attachment .attachment-image-view{
+	border-radius: 3px;
+	box-shadow: 0 0 10px 1px rgba(0,0,0,0.1);
+}
+.module-issue .comment-attachment .attachment-file-view{
+	width:150px;
+	padding-top:20px;
+	overflow:hidden;
+}
+.module-issue .comment-attachment .attachment-file-view i{
+	font-size: 80px;
+	text-align: center;
+	display: block;
+}
+.module-issue .comment-attachment .attachment-file-view h6{
+	font-size: 10px;
+	font-weight: bold;
+	height: 35px;
+	text-align: center;
+	width: 100px;
+	margin:  5px auto auto auto;
+	overflow:hidden;
+	color:#cecece;
+	text-transform: uppercase;
+}
+.module-issue .attachment-file .attachment-image-view,
+.module-issue .attachment-image .attachment-file-view{
+	display: none;
+}

+ 110 - 0
plugin/issue/issue.plugin.php

@@ -0,0 +1,110 @@
+<?php
+//Cette fonction va generer une page quand on clique sur issue dans menu
+function issue_page(){
+	global $_,$myUser;
+	if(!isset($_['module']) || $_['module'] !='issue') return;
+	$page = !isset($_['page']) ? 'sheet.report' : $_['page'];
+	$file = __DIR__.SLASH.'page.'.$page.'.php';
+	if(!file_exists($file)) throw new Exception("Page ".$page." inexistante");
+	
+	require_once($file);
+}
+
+function issue_modal(){
+	global $_,$myUser;
+	if($myUser->connected())
+		require_once(__DIR__.SLASH.'modal.issue.report.php');
+}
+
+//Fonction executée lors de l'activation du plugin
+function issue_install($id){
+	if($id != 'fr.idleman.issue') return;
+	Entity::install(__DIR__);
+}
+
+//Fonction executée lors de la désactivation du plugin
+function issue_uninstall($id){
+	if($id != 'fr.idleman.issue') return;
+	Entity::uninstall(__DIR__);
+}
+
+//Déclaration des sections de droits du plugin
+function issue_section(&$sections){
+	$sections['issue'] = "Gestion des droits sur le plugin issue";
+	// $sections['issuereport'] = "Gestion des droits sur l'entité issuereport";
+}
+
+//cette fonction comprends toutes les actions du plugin qui ne nécessitent pas de vue html
+function issue_action(){
+	require_once(__DIR__.SLASH.'action.php');
+}
+
+//Déclaration du menu de réglages
+function issue_menu_setting(&$settingMenu){
+	global $_, $myUser;
+	
+	if(!$myUser->can('issue','configure')) return;
+	$settingMenu[]= array(
+		'sort' =>1,
+		'url' => 'setting.php?section=global.report',
+		'icon' => 'fas fa-angle-right',
+		'label' => 'Issues'
+	);
+}
+
+//Déclaration categorie de notification
+function issue_notification_type(&$types){
+	$types['issue'] = array(
+		'category' =>'Développement',
+		'label' =>'Déclaration de tickets',
+		'color' =>'#dc3545',
+		'icon'  =>'fas fa-bug',
+		'description' => "Notification lorsqu'un ticket est ouvert par un utilisateur",
+		'default_methods' => array(
+			'interface' => true,
+			'mail' => true
+		)
+	);
+}
+
+//Déclaration des pages de réglages
+function issue_content_setting(){
+	global $_;
+	if(file_exists(__DIR__.SLASH.'setting.'.$_['section'].'.php'))
+		require_once(__DIR__.SLASH.'setting.'.$_['section'].'.php');
+}
+
+//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('issue',array(
+    'issue_report_mails' => array(
+    	"label"=>"Destinataires des ouvertures de tickets",
+    	"legend"=>"Utilisateurs ou rangs qui recevront la notification d'ouverture de nouveaux tickets",
+    	"type"=>"user",
+    	"placeholder"=>"eg. Administrateur",
+    	"parameters"=>array(
+    		"data-multiple"=>true,
+    		"data-types"=>"user,rank"
+    	)
+    ),
+));
+
+//Déclation des assets
+Plugin::addCss("/css/main.css"); 
+Plugin::addCss("/css/component.css",true); 
+Plugin::addJs("/js/main.js"); 
+Plugin::addJs("/js/component.js",true); 
+Plugin::addJs("/js/html2canvas.min.js"); 
+
+//Mapping hook / fonctions
+Plugin::addHook("install", "issue_install");
+Plugin::addHook("uninstall", "issue_uninstall"); 
+Plugin::addHook("section", "issue_section");
+Plugin::addHook("page", "issue_page");   
+Plugin::addHook("application_bottom", "issue_modal");   
+Plugin::addHook("action", "issue_action");  
+Plugin::addHook("menu_setting", "issue_menu_setting");    
+Plugin::addHook("content_setting", "issue_content_setting");   
+
+Plugin::addHook("notification_type", "issue_notification_type"); 
+?>

+ 70 - 0
plugin/issue/js/component.js

@@ -0,0 +1,70 @@
+function init_components_tagcloud(input){
+
+	if(!input.data("data-component")) {
+		
+		var html = '<ul class="data-type-tagcloud" data-source="'+input.attr('id')+'">';
+		var tags = input.data('tags');
+		
+		for(var key in tags){
+			var tag = tags[key];
+			html += '<li><span class="badge badge-tag" data-slug="'+key+'" data-color="'+tag.color+'"><i class="'+tag.icon+'"></i> '+tag.label+'</span></li>';
+		}
+		html += '</ul>';
+		var picker = $(html); 
+		input.before(picker);
+		input.data("data-component",picker);
+
+
+		
+		picker.find('.badge-tag').click(function(){
+
+			if(input.attr("readonly") == "readonly") return;
+
+			var selected = input.val().split(',');
+
+			//Supprimer les valeurs vides
+			selected = selected.filter(function(e){return e});
+			var element = $(this);
+			if(!element.hasClass('active')){
+				$(this).addClass('active').css({
+					background : element.attr('data-color'),
+					color : '#ffffff',
+					border : '1px solid '+ element.attr('data-color')
+				});
+				selected.push(element.attr('data-slug'));
+			} else {
+				$(this).removeClass('active').css({
+					background : '',
+					color : '',
+					border : ''
+				});
+				var index = selected.indexOf(element.attr('data-slug'));
+				if(index > -1 ) selected.splice(index, 1);
+			}
+			input.val(selected.join(','));
+			input.trigger('change').trigger('click');
+		});
+	
+
+		if(input.parent().hasClass("input-group")){
+			input.parent().after(input.detach());
+			input.before('<div id="business-anchor" class="dropdown-anchor"></div>');
+		}
+		input.addClass('hidden');
+	}else{
+		picker = input.data("data-component");
+	}
+	//Si le champ input est ré-rempli, on met a jour les tags
+	if(input.val()!=''){
+		var selected = input.val().split(',');
+		for(var key in selected){
+			var tag = selected[key];
+			var tagElement = picker.find('.badge-tag[data-slug="'+tag+'"]');
+			tagElement.addClass('active').css({
+				background : tagElement.attr('data-color'),
+				color : '#ffffff',
+				border : '1px solid '+ tagElement.attr('data-color')
+			});
+		}
+	}
+}

File diff suppressed because it is too large
+ 5 - 0
plugin/issue/js/html2canvas.min.js


+ 306 - 0
plugin/issue/js/main.js

@@ -0,0 +1,306 @@
+
+window.ajax_calls = [];
+window.issue_data = {};
+
+$.real_action = $.action;
+
+$.action = function(data,success,error,progress) {
+	if (!Date.now)  Date.now = function() { return new Date().getTime(); }
+   ajax_calls.push({request : data,time:Date.now() });
+   if(ajax_calls.length >10) ajax_calls.shift();
+   $.real_action(data,success,error,progress);
+}
+
+//CHARGEMENT DE LA PAGE
+function init_plugin_issue(){
+	window.documentLoaded = false;
+	switch($.urlParam('page')){
+
+		case 'sheet.report':
+			if($('.issue').hasClass('readonly')){
+				$('#assign,#reportState,#tags').attr('readonly','readonly');
+				init_components();
+			}
+			issue_issue_event_search();
+		break;
+		default:
+		break;
+	}	
+	window.documentLoaded = true;
+}
+
+function init_setting_global_report(){
+	$('#issuereports').sortable_table({
+		onSort : issue_issuereport_search
+	});
+}
+
+function issue_add(){
+	var button = $('.issue-declare-button');
+	if(button.attr('data-disabled')=='1') return;
+
+	button.attr('data-disabled',1);
+	button.find('i.fa-bug').addClass('hidden');
+	button.find('i.fa-cog').removeClass('hidden');
+
+	issue_data.height = window.screen.availHeight;
+	issue_data.width = window.screen.availWidth;
+	issue_data.browser = '';
+
+	//on utilise pas navigator.appName qui est un fake la plupart du temps
+	issue_data.browser = issue_detect_browser();
+
+	issue_data.browserVersion = navigator.appVersion;
+	issue_data.online = navigator.onLine;
+	issue_data.os = navigator.platform;
+	issue_data.from = document.location.href;
+	issue_data.history = JSON.stringify(ajax_calls);
+
+	var modal = $('#issue-report-modal');
+	reset_inputs(modal);
+	$('.issue-screenshot').addClass('hidden').children().remove();
+	$('span.badge.badge-tag.active', modal).removeAttr('style').removeClass('active');
+	init_components('#issue-report-modal');
+	modal.modal({
+	    backdrop: 'static',
+	    show: true
+	});
+
+	modal.on('hidden.bs.modal', function () {
+	    button.removeAttr('data-disabled');
+	});
+	
+	for(var key in issue_data){
+		var element = $('[data-uid="'+key+'"]', modal);
+		if(element.length == 0) continue;
+		element.html(issue_data[key] == true ? 'OUI': issue_data[key]);
+	}
+
+	button.find('i.fa-cog').addClass('hidden');
+	button.find('i.fa-bug').removeClass('hidden');
+}
+
+function issue_screenshot(){
+	//Check if IE
+    if (window.navigator.userAgent.indexOf("MSIE ") > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./)){
+    	alert('Unsupported html2canvas librairy (using ES6 features)');
+    	console.warn('Unsupported html2canvas librairy (using ES6 features)');
+    	return;
+    }
+
+	$('#issue-report-modal').modal('hide');
+	setTimeout(function(){
+		html2canvas(document.querySelector("html")).then(function(canvas){
+		 	$('#issue-report-modal').modal('show');
+		    issue_data.screenshot = canvas.toDataURL('image/jpg');
+		    $('.issue-screenshot').removeClass('hidden').html('<img class="pointer" src="'+issue_data.screenshot+'"/>');
+		     $('.issue-screenshot img').click(function(){
+		     	var image = new Image();
+		        image.src = issue_data.screenshot;
+		        var popup = window.open("");
+		        popup.document.write(image.outerHTML);
+		     });
+		});
+	},500);
+}
+
+function issue_send(element){
+	var sendButton = $(element);
+	var closeButton = sendButton.closest('.modal-footer').find('.close-button');
+	var modal = $('#issue-report-modal');
+	var button = $('.issue-declare-button');
+
+	sendButton.attr('disabled', true);
+	closeButton.attr('disabled', true);
+	sendButton.html('<i class="fas fa-spinner fa-pulse"></i> En cours d\'envoi...');
+
+	issue_data.action = 'issue_issuereport_save';
+	issue_data.tags = $('.issue-modal-tags').val();
+	data = $.extend(issue_data,$('#issue-report-modal').toJson());
+
+	$.action(data,function(response){
+		issue_data = {};
+		reset_inputs('#issue-report-modal');
+		init_components('#issue-report-modal');
+
+		sendButton.html('<i class="far fa-paper-plane"></i> Envoyer');
+		sendButton.attr('disabled', false);
+		closeButton.attr('disabled', false);
+
+		modal.modal('hide');
+		$.message('info','Rapport envoyé<br/><a class="btn btn-mini text-light" href="index.php?module=issue&page=sheet.report&id='+response.item.id+'"> &gt; Consulter le rapport</a>',10000);
+	},function(error){
+		button.removeAttr('data-disabled');
+		sendButton.html('<i class="far fa-paper-plane"></i> Envoyer');
+		sendButton.attr('disabled', false);
+		closeButton.attr('disabled', false);
+		sendButton.removeClass('hidden');
+	});
+}
+
+function issue_detect_browser(){
+	// Opera 8.0+
+	var isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
+	// Firefox 1.0+
+	var isFirefox = typeof InstallTrigger !== 'undefined';
+	// Safari 3.0+ "[object HTMLElementConstructor]" 
+	var isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification));
+	// Internet Explorer 6-11
+	var isIE = /*@cc_on!@*/false || !!document.documentMode;
+	// Edge 20+
+	var isEdge = !isIE && !!window.StyleMedia;
+	// Chrome 1 - 68
+	var isChrome = Boolean(window.chrome);
+	// Blink engine detection
+	var isBlink = (isChrome || isOpera) && !!window.CSS;
+
+	if(isOpera) return 'opera';
+	if(isFirefox) return 'firefox';
+	if(isChrome) return 'chrome';
+	if(isSafari) return 'safari';
+	if(isIE) return 'ie';
+	if(isEdge) return 'edge';
+	if(isBlink) return 'blink';
+	return 'inconnu';
+}
+
+//Enregistrement des configurations
+function issue_setting_save(){
+	$.action({ 
+		action : 'issue_setting_save', 
+		fields : $('#issue-setting-form').toJson() 
+	},function(){
+		$.message('success','Enregistré');
+	});
+}
+
+/** ISSUEREPORT **/
+//Récuperation d'une liste de issuereport dans le tableau #issuereports
+function issue_issuereport_search(callback){
+	$('#issuereports').fill({
+		action:'issue_issuereport_search',
+		filters : $('#filters').filters(),
+		sort : $('#issuereports').sortable_table('get'),
+		tags : $('#report-tags').val()
+	},function(response){
+
+		if(callback!=null) callback();
+	});
+}
+//changement d'une meta de ticket (état, assiignation, tags...) 
+function issue_issuereport_meta_save(){
+	if(!window.documentLoaded) return;
+	$.action({
+		action : 'issue_issuereport_meta_save',
+		id : $('#issuereport-form').attr('data-id'),
+		assign : $('#assign').val(),
+		tags : $('#tags').val(),
+		state : $('#reportState').val()
+	},function(){
+		issue_issue_event_search();
+	});
+}
+
+//Suppression d'élement issuereport
+function issue_issuereport_delete(element){
+	if(!confirm('Êtes-vous sûr de vouloir supprimer ce ticket ?')) return;
+	var line = $(element).closest('tr');
+	$.action({
+		action : 'issue_issuereport_delete',
+		id : line.attr('data-id')
+	},function(r){
+		line.remove();
+		$.message('info','Ticket supprimé');
+	});
+}
+
+
+
+
+
+/** ISSUEEVENT **/
+	
+//Récuperation d'une liste de issueevent dans le tableau #issueevents
+function issue_issue_event_search(callback){
+	var stateTpl = $('.issue-state.hidden:eq(0)').get(0).outerHTML;
+	var assignationTpl = $('.issue-assignation.hidden:eq(0)').get(0).outerHTML;
+	var tagTpl = $('.issue-tag.hidden:eq(0)').get(0).outerHTML;
+	$('.issue-events').fill({
+		differential :  true,
+		action:'issue_issue_event_search',
+		issue : $('#issuereport-form').attr('data-id'),
+		templating : function(data,element,defaultTpl){
+
+			if(data.type == 'state') return stateTpl;
+			if(data.type == 'assignation') return assignationTpl;
+			if(data.type == 'tag') return tagTpl;
+			return defaultTpl;
+		},
+		showing : function(item,i){
+			item.css({
+				transform:'scale(0)',
+				transition:'all 0.2s ease-in-out',
+				opacity : 0
+			}).removeClass('hidden');
+			setTimeout(function(){
+				item.css({
+					transform:'scale(1)',
+					opacity : 1
+				})
+			},(i+1)*10);
+		}
+	},function(response){
+	
+		if(callback!=null) callback();
+	});
+}
+
+//Ajout ou modification d'élément issueevent
+function issue_issue_event_save(){
+	var data = $('#issue-event-form').toJson();
+	data.issue = $('#issuereport-form').attr('data-id');
+	$.action(data,function(r){
+		$('#issue-event-form').attr('data-id','');
+		$('#content').trumbowyg('html','');
+		issue_issue_event_search();
+		$.message('success','Enregistré');
+	});
+}
+
+
+//Récuperation ou edition d'élément issueevent
+function issue_issue_event_edit(element){
+	var line = $(element).closest('.issue-event');
+	$.action({action:'issue_issue_event_edit',id:line.attr('data-id')},function(r){
+		$.setForm('#issue-event-form',r);
+		$('#issue-event-form').attr('data-id',r.id);
+		$('#content').trumbowyg('html',r.content);
+		init_components();
+		window.location = '#issue-event-form';
+	});
+}
+
+//Suppression d'élement issueevent
+function issue_issue_event_delete(element){
+
+	if(!confirm('Êtes vous sûr de vouloir supprimer cet item ?')) return;
+	var line = $(element).closest('.issue-event');
+
+	if(line.hasClass('issue-first-comment')){
+		if(!confirm('Si vous supprimez le dernier commentaire, l\'intégralité du rapport sera supprimé')) return;
+	}
+	$.action({
+		action : 'issue_issue_event_delete',
+		id : line.attr('data-id')
+	},function(r){
+		line.css({
+			transform : 'scale(0)',
+			opacity:0
+		});
+		setTimeout(function(){
+			line.remove();
+			if(r.redirect) window.location = r.redirect;
+		},200);
+		
+	});
+}

+ 103 - 0
plugin/issue/mail.template.php

@@ -0,0 +1,103 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+	<meta http-equiv="X-UA-Compatible" content="IE=edge">
+
+	<title></title>
+
+	<style type="text/css">
+	/* CLIENT-SPECIFIC STYLES */
+	body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+	table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+	img { -ms-interpolation-mode: bicubic; }
+
+	/* RESET STYLES */
+	img { border: 0; outline: none; text-decoration: none; }
+	table { border-collapse: collapse !important; }
+	body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
+
+	/* iOS BLUE LINKS */
+	a[x-apple-data-detectors] {
+		color: inherit !important;
+		text-decoration: none !important;
+		font-size: inherit !important;
+		font-family: inherit !important;
+		font-weight: inherit !important;
+		line-height: inherit !important;
+	}
+
+	/* ANDROID CENTER FIX */
+	div[style*="margin: 16px 0;"] { margin: 0 !important; }
+
+	/* MEDIA QUERIES */
+	@media all and (max-width:639px){ 
+		.wrapper{ width:320px!important; padding: 0 !important; }
+		.container{ width:300px!important;  padding: 0 !important; }
+		.mobile{ width:300px!important; display:block!important; padding: 0 !important; }
+		.img{ width:100% !important; height:auto !important; }
+		*[class="mobileOff"] { width: 0px !important; display: none !important; }
+		*[class*="mobileOn"] { display: block !important; max-height:none !important; }
+	}
+
+</style>    
+</head>
+<body style="margin:0; padding:0; background-color:#F2F2F2;">
+	<center>
+		<table width="100%" border="0" cellpadding="0" cellspacing="0" bgcolor="#F2F2F2">
+			<tr>
+				<td align="center" valign="top">
+					<!-- grids -->   
+					<table width="640" cellpadding="0" cellspacing="0" border="0" class="wrapper" bgcolor="#FFFFFF">
+						<tr>
+							<td height="5" bgcolor="#FFC107" style="font-size:5px; line-height:5px;">&nbsp;</td>
+						</tr>
+						<tr>
+							<td height="10" style="font-size:10px; line-height:10px;">&nbsp;</td>
+						</tr>
+						<tr>
+							<td align="center" valign="top">
+
+								<table width="600" cellpadding="0" cellspacing="0" border="0" class="container">
+									<tr>
+										<td align="center" valign="top">
+											<h3>Bonjour, le ticket <a href="{{url}}">#{{id}}</a> a été ouvert par {{creator}}</h3>
+											<p>Veuillez trouver ci dessous les informations principales:</p>
+											<ul align="left" style="text-align:left;">
+												<li>Navigateur : <strong>{{browser}}</strong></li>
+												<li>Auteur : <strong>{{creator}}</strong></li>
+											</ul>
+											<strong>Page : {{from}} </strong><br>
+											{{comment}}
+											<br>
+											<br>
+											<p>Pour consulter l'ensemble des informations, cliquez sur le lien ci dessous.</p>
+
+											<table width="200" height="44" cellpadding="0" cellspacing="0" border="0" bgcolor="#ff074e" style="border-radius:4px;">
+												<tr>
+													<td align="center" valign="middle" height="44" style="font-family: Arial, sans-serif; font-size:14px; font-weight:bold;">
+														<a href="{{url}}" target="_blank" style="font-family: Arial, sans-serif; color:#ffffff; display: inline-block; text-decoration: none; line-height:44px; width:200px; font-weight:bold;">Voir le ticket</a>
+													</td>
+												</tr>
+											</table>
+											<br/>
+											
+
+										</td>
+									</tr>
+								</table>
+
+							</td>
+						</tr>
+						<tr>
+							<td height="10" style="font-size:10px; line-height:10px;">&nbsp;</td>
+						</tr>
+					</table>  
+					<!-- /grids -->
+				</td>
+			</tr>
+		</table>
+	</center>
+</body>
+</html>

+ 66 - 0
plugin/issue/modal.issue.report.php

@@ -0,0 +1,66 @@
+<?php 
+require_once(__DIR__.SLASH.'IssueReportTag.class.php');
+$tags = IssueReportTag::tags();
+?>
+
+<div class="issue-declare-button noPrint" onclick="issue_add();" title="Déclarer un bug"><i class="fas fa-bug"></i><i class="fas fa-cog fa-spin hidden"></i></div>
+<!-- Modal -->
+<div class="modal fade issue-report-modal" id="issue-report-modal" tabindex="-1" role="dialog" aria-labelledby="modal-issue-report-label" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title" id="modal-issue-report-label">Nouveau ticket (Bug / Demandes) :</h4>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <div class="row issue-form">
+                    <div class="col-md-8">
+                        <div class="row">
+                            <div class="col-md-12">
+                                <h5 class="d-inline-block mr-2 w-auto">Sélectionnez une catégorie :</h5>
+                                <input type="text" data-tags='<?php echo json_encode($tags); ?>' class="issue-modal-tags no-select" data-type="tagcloud">
+                            </div>
+                            <div class="col-md-12"><hr></div>
+                            <div class="col-md-8">
+                                <h5 class="d-inline-block mr-2 w-auto">Commentez votre ticket :<small class="text-muted"> (eg. votre demande, étapes pour arriver à l'erreur, etc...)</small></h5>
+                                <textarea data-type="wysiwyg" id="issue-comment" class="form-control mt-0"></textarea>
+                            </div>
+                            <div class="col-md-4">
+                                <h5 class="d-inline-block mr-2 w-auto">Informations :</h5>
+                                <ul  class="list-group">
+                                    <li class="list-group-item">
+                                        <div class="mb-1 font-weight-bold">Page :</div>
+                                        <u class="text-info default"><span class="page-link-issue" data-uid="from"></span></u>
+                                    </li>
+                                    <li class="list-group-item">
+                                        <div class="mb-1 font-weight-bold">Navigateur : </div>
+                                        <span data-uid="browser"></span> <small class="text-muted" data-uid="browserVersion"></small>
+                                    </li>
+                                    <li class="list-group-item">
+                                        <div class="font-weight-bold d-inline-block">Os : </div>
+                                        <span class="badge badge-primary badge-pill" data-uid="os"></span>
+                                    </li>
+                                    <li class="list-group-item">
+                                        <div class="font-weight-bold d-inline-block">Internet : </div>
+                                        <span class="badge badge-primary badge-pill" data-uid="online"></span>
+                                    </li>
+                                </ul>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-md-4">
+                        <h5 class="d-inline-block mr-2 w-auto">Fichiers joints :</h5>
+                        <div onclick="issue_screenshot()" class="btn d-inline-block p-0 float-right"><i class="fas fa-camera-retro"></i> <small class="text-muted">Prendre une capture d'écran</small></div>
+                        <div data-type="dropzone" data-label="Faites glisser vos fichiers supplémentaires ici" data-allowed="jpeg,jpg,bmp,gif,png,xlsx,docx,pdf" class="form-control mt-2" id="document" name="document"></div>
+                        <div class="issue-screenshot hidden"></div>
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="close-button btn btn-light" data-dismiss="modal">Fermer</button>
+                    <button type="button" id="issue-send" class="btn btn-primary" onclick="issue_send(this);"><i class="far fa-paper-plane"></i> Envoyer</button>
+                </div>
+            </div>
+        </div>
+    </div>

+ 206 - 0
plugin/issue/page.sheet.report.php

@@ -0,0 +1,206 @@
+<?php 
+if(!$myUser->connected()) throw new Exception("Vous devez être connecté pour accéder à cette fonctionnalité",401);
+require_once(__DIR__.SLASH.'IssueReport.class.php');
+require_once(__DIR__.SLASH.'IssueReportTag.class.php');
+
+$tags = IssueReportTag::tags();
+$issuereport = IssueReport::provide();
+if(!isset($issuereport->id)) throw new Exception("Impossible d'accéder à un rapport vierge");
+if(!$myUser->can('issue','read') && $myUser->login != $issuereport->creator) throw new Exception("Vous n'avez pas la permission pour executer cette fonctionnalité",403);
+$state = IssueReport::states($issuereport->state);
+
+$osIcon = $browserIcon = 'fas fa-question-circle';
+if(strlen($issuereport->os)>=3 && substr(strtolower($issuereport->os),0,3) == 'win') $osIcon = 'fab fa-windows text-primary';
+if(strlen($issuereport->os)>=5 && substr(strtolower($issuereport->os),0,5) == 'linux') $osIcon = 'fab fa-linux text-danger';
+if(strlen($issuereport->os)>=3 && substr(strtolower($issuereport->os),0,3) == 'mac') $osIcon = 'fab fa-apple text-secondary';
+
+switch($issuereport->browser){
+	case 'firefox': $browserIcon = 'fab fa-firefox text-warning'; break;
+	case 'ie': $browserIcon = 'fab fa-internet-explorer text-danger'; break;
+	case 'edge': $browserIcon = 'fab fa-edge text-primary'; break;
+	case 'chrome': $browserIcon = 'fab fa-chrome text-success'; break;
+}
+
+$issueTags = array();
+foreach(IssueReportTag::loadAll(array('report'=>$issuereport->id)) as $tag):
+	$issueTags[] = $tag->tag;
+endforeach;
+
+?>
+<div class="issue <?php echo !$myUser->can('issue','edit')?'readonly':''; ?>">
+
+
+	
+	<div id="issuereport-form" class="row issuereport-form  justify-content-md-center"  data-action="issue_issuereport_save" data-id="<?php echo $issuereport->id; ?>">
+		<div class="col-md-10">
+
+			<div class="row">
+				<div class="col-md-9 p-3">
+					<h2 class="d-inline-block mb-0">Ticket #<?php echo $issuereport->id; ?></h2>
+				</div>
+				<div class="col-md-3 p-3">
+					<a href="setting.php?section=global.report" class="btn btn-light w-100"><i class="fas fa-reply"></i> Revenir aux tickets</a>
+				</div>
+			</div>
+
+
+			<div class="row">
+
+				<!-- events -->
+				<div class="col-md-9 issue-event-bar">
+
+					<ul class="issue-events">
+						<li class="issue-event issue-comment hidden {{classes}}" data-id="{{id}}">
+							<div class="comment-header">
+								<img class="avatar-medium avatar-rounded" src="{{avatar}}"> 
+								<div class="header-infos">
+									<span class="d-inline-block  font-weight-bold"> {{fullName}}</span> <br>
+									<small class="text-muted">{{createdRelative}}</small> 	
+								</div>
+								<ul class="only-editable right">
+									<li class="text-muted edit-issue-comment" onclick='issue_issue_event_edit(this);'><i class="fas fa-pencil-alt"></i></li>
+									<li class="text-muted delete-issue-comment" onclick='issue_issue_event_delete(this);'><i class="far fa-trash-alt"></i></li>
+								</ul>
+							</div>
+
+							<div class="comment-message">
+								{{{comment}}}
+							</div>
+
+							{{#hasfiles}}
+							<div class="comment-attachments">
+								<h5>PIECES JOINTES </h5>
+								{{#files}}
+								
+								<a href="{{url}}"  class="comment-attachment attachment-{{type}}" target="_blank">
+									
+									<img class="attachment-image-view"  src="{{url}}">
+									<div class="attachment-file-view"><i class="{{icon}}"></i> <h6 title="{{label}}">{{labelExcerpt}}</h6></div>
+								
+								</a>
+								{{/files}}
+							</div>
+							{{/hasfiles}}
+						</li>
+						
+						<li class="issue-event issue-state hidden" data-id="{{id}}">
+							<img class="avatar-mini avatar-rounded" src="{{avatar}}"> <span class="d-inline-block  font-weight-bold"> {{fullName}}</span>
+							<i>a changé l'état du ticket  </i>
+							de <span class="badge badge-secondary" style="background: {{oldstate.color}}"><i class="{{oldstate.icon}}"></i> {{oldstate.label}}</span> 
+							en <span class="badge badge-secondary" style="background: {{state.color}}"><i class="{{state.icon}}"></i> {{state.label}}</span> 
+							<small class="text-muted">{{createdRelative}}</small> 	
+						</li>
+						
+						<li class="issue-event issue-assignation hidden" data-id="{{id}}">
+							<img class="avatar-mini avatar-rounded" src="{{avatar}}"> <span class="d-inline-block  font-weight-bold"> {{fullName}}</span>
+							<i>a assigné le ticket à</i> <img class="avatar-mini avatar-rounded" src="{{assigned.avatar}}"> <span class="d-inline-block  font-weight-bold"> {{assigned.fullName}}</span>
+							<small class="text-muted">{{createdRelative}}</small> 	
+						</li>
+						<li class="issue-event issue-tag hidden" data-id="{{id}}">
+							<img class="avatar-mini avatar-rounded" src="{{avatar}}"> <span class="d-inline-block  font-weight-bold"> {{fullName}}</span>
+							<i>a {{action}} le tag</i> <span class="badge badge-secondary ml-1" style="background: {{tag.color}}"><i class="{{tag.icon}}"></i> {{tag.label}}</span> 
+							<small class="text-muted">{{createdRelative}}</small> 	
+						</li>
+					</ul>
+
+
+					<div id="issue-event-form" class="issue-event-form" data-action="issue_issue_event_save" data-id="">
+						<div class="post-header">
+							<img class="avatar-mini avatar-rounded mr-1" src="<?php echo $myUser->getAvatar(); ?>"> 
+							<span class="font-weight-bold"> <?php echo $myUser->fullName(); ?></span> 
+							<small class="text-muted"><?php echo date('d/m/y H:i'); ?></small> 	
+						</div>
+						<textarea id="content" data-type="wysiwyg" name="content" class="form-control mt-2" placeholder="Répondre au sujet..."></textarea>
+
+						<!--<div  class="mt-1" data-type="dropzone" data-preview data-delete="" data-allowed="" data-label="Ajouter fichiers, images..." ></div>-->
+
+						<div onclick="issue_issue_event_save();" class="btn btn-success right"><i class="fas fa-check"></i> Envoyer</div>
+						<div class="clear"></div>
+					</div>
+
+
+				</div>
+
+
+				<!-- sidebar -->
+				<div class="col-md-3 issue-sidebar">
+
+					
+
+					<h5 class="text-muted">TAGS</h5>
+					<input type="text" data-tags='<?php echo json_encode($tags); ?>' value="<?php echo implode(',',$issueTags); ?>" class="issue-modal-tags no-select" data-type="tagcloud"  id="tags" onchange="issue_issuereport_meta_save()">
+
+					<hr>
+
+					<select class="w-100" data-type="dropdown-select" id="reportState" onchange="issue_issuereport_meta_save()">
+						<?php foreach(IssueReport::states() as $slug=>$state): ?>
+							<option <?php echo $slug==$issuereport->state ? 'selected="selected"': ''; ?> value="<?php echo $slug; ?>" style="background-color:<?php echo $state['color']; ?>;color:#ffffff;" data-icon="<?php echo $state['icon']; ?>"><?php echo $state['label']; ?></option>
+						<?php endforeach; ?>
+					</select>
+
+					<hr>
+
+					<h5 class="text-muted">ASSIGNATION</h5>
+
+					<input type="text" class="form-control" data-type="user" id="assign" value="<?php echo $issuereport->assign; ?>" onchange="issue_issuereport_meta_save()">
+
+
+
+
+					<hr>
+
+					<h5 class="text-muted">ENVIRONNEMENT</h5>
+					<ul class="list-group">
+						<li class="list-group-item">  
+							<i class="far fa-file-code" title="Page"></i>
+							<a href="<?php echo str_replace(ROOT_URL,'',$issuereport->from); ?>" title="Accéder à la page" class=""><?php echo str_replace(ROOT_URL,'',$issuereport->from); ?></a><br>
+						</li>
+						<li class="list-group-item">
+							<i class="<?php echo $browserIcon; ?>" title="Navigateur"></i>
+							<strong><?php echo $issuereport->browser; ?></strong><br>
+							<small class="text-muted"><?php echo $issuereport->browserVersion; ?></small>
+						</li>
+						<li class="list-group-item"> 
+							<i class="<?php echo $osIcon; ?>" title="Os"></i>
+							<strong><?php echo $issuereport->os; ?></strong>
+						</li>
+						<li class="list-group-item">
+							<i class="fas fa-tv" title="Écran"></i>
+							<strong><?php echo $issuereport->width; ?>px * <?php echo $issuereport->height; ?>px</strong>
+						</li>
+						<li class="list-group-item">IP : 
+							<strong><?php echo $issuereport->ip; ?></strong>
+						</li>
+					</ul>
+
+					<?php 
+
+					$histories = json_decode($issuereport->history,true);
+					
+					if($histories!=false): 
+
+					?>
+					<hr>
+
+					<h5 class="text-muted">5 ACTIONS AJAX</h5>
+
+					<ul class="list-group">
+						<?php  foreach ($histories as $history): 
+							preg_match("/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/i", $history['time'], $matches);
+							$date = isset($matches[1]) ? date("d/m/Y", strtotime($matches[1])) : '-';
+							?>
+							<li class="list-group-item">
+								<small class="text-muted"><?php echo $date.' - '.(isset($matches[2])? $matches[2]: '-'); ?></small><br>
+								<pre><?php print_r($history['request']); ?></pre>
+							</li>
+						<?php endforeach; ?>
+					</ul>
+					<?php endif; ?>
+	
+
+				</div>
+			</div>
+		</div>
+	</div>
+</div><br>
+

+ 117 - 0
plugin/issue/setting.global.report.php

@@ -0,0 +1,117 @@
+<?php
+global $myUser;
+User::check_access('issue','configure');
+require_once(__DIR__.SLASH.'IssueReport.class.php');
+require_once(__DIR__.SLASH.'IssueReportTag.class.php');
+
+$browsers = array(
+    'opera' => 'Opera',
+    'firefox' => 'Mozilla Firefox',
+    'chrome' => 'Google Chrome',
+    'safari' => 'Safari',
+    'ie' => 'Internet Explorer',
+    'edge' => 'Microsoft Edge',
+    'blink' => 'Blink'
+);
+$states = array();
+foreach(IssueReport::states() as $slug=>$state)
+    $states[$slug] = $state['label'];
+
+$tags = IssueReportTag::tags();
+?>
+
+<div class="row">
+    <div class="col-xl-12 tab-container parent-tab mb-3 noPrint">
+        <h3>Réglages Tickets</h3>
+       
+        <ul class="nav nav-tabs" role="tablist">
+            <li class="nav-item"><a data-toggle="tab" class="nav-link active" href="#tab-reports" aria-controls="tab-reports" aria-selected="true">Listing des rapports</a></li>
+            <li class="nav-item"><a data-toggle="tab" class="nav-link" href="#tab-settings" aria-controls="tab-settings" aria-selected="false">Paramètres</a></li>
+        </ul>
+    </div>
+
+    <div class="tab-content col-xl-12">
+        <!-- Onglet Listing des rapports -->
+        <div class="tab-pane show active in" id="tab-reports" role="tabpanel" aria-labelledby="tab-reports">
+            
+            <div class="issue-reports-search mb-2">
+                <select id="filters" data-join="and" data-slug="issue_search" data-type="filter" data-label="Recherche" data-function="issue_issuereport_search">
+                    <option value="state" data-filter-type="dictionnary" data-filter-source='<?php echo json_encode($states); ?>'>Etat</option>
+                    <option value="from" data-filter-type="text">Url</option>
+                    <option value="comment" data-filter-type="text">Commentaire</option>
+                    <option value="{{table}}.creator" data-filter-type="user">Auteur</option>
+                    <option value="{{table}}.created" data-filter-type="date">Date</option>
+                    <option value="browser" data-filter-type="dictionnary" data-filter-source='<?php echo json_encode($browsers); ?>'>Navigateur</option>
+                    <option value="ip" data-filter-type="text">Adresse IP</option>
+                </select>
+                <div>
+                    <label class="-d-inline-block mt-1 mb-1">Tags :</label>
+                    <input type="text" onchange="issue_issuereport_search()" data-tags='<?php echo json_encode($tags); ?>' id="report-tags" data-type="tagcloud">
+                </div>
+            </div>
+
+          
+            <ul id="issuereports" class="list-issue-reports" data-entity-search="issue_issuereport_search">
+                
+                    <li data-id="{{id}}" class="hidden">
+                       <a>
+                       <div class="issue-state">
+                            <i class="{{state.icon}}" style="color: {{state.color}};" title="{{state.label}}"></i>
+                        </div>
+                        <div class="infos-cell text-left">
+                            <a  class="issue-title" title="Voir le rapport détaillé" href="index.php?module=issue&page=sheet.report&id={{id}}">
+                                <span class="font-weight-bold d-inline-block mb-1">{{excerpt}}</span>
+                            </a>
+                            {{#assign}}<span class="assign"> Assigné à <i class="ml-1 far fa-meh-blank"></i> {{assign}}</span>{{/assign}}
+                            <div class="mb-1 mt-2 text-muted">
+                                <a class="pointer font-weight-bold  mr-2" onclick="$(this).parent().next('div').slideToggle(150)" title="Afficher plus de détails">#{{id}}</a> <i class="far fa-calendar"></i> {{date}} <i class="far fa-clock ml-2"></i> {{hour}} par <span class=""><i class="far fa-meh-blank"></i> {{creator}}</span> 
+                                <ul class="report-tag ml-2">
+                                    {{#tags}}
+                                        <li>
+                                            <span class="badge badge-tag no-select" style="background-color:{{color}}; border:1px solid {{color}};color:#ffffff;"><i class="{{icon}}"></i> {{label}}</span>
+                                        </li>
+                                    {{/tags}}
+                                </ul>
+                            </div>
+                            <div class="hidden">
+                                <span> Page : <a title="Accéder à la page" href="<?php echo ROOT_URL; ?>{{relativefrom}}" target="_blank">{{relativefrom}}</a></span><br>
+                                <small class="text-muted mb-2 d-block">IP: {{ip}}</small>
+                                <div>
+                                    <span class="mr-1"><i class="{{browserIcon}}"></i> {{browser}}</span>|
+                                    <span class="ml-1 mr-1"><i class="{{osIcon}}"></i> {{os}}</span>|
+                                    <span class="ml-1"><i class="fas fa-tv"></i> {{width}}px * {{height}}px</span>
+                                </div>
+                                <small class="text-muted">{{browserVersion}}</small>
+                            </div>
+                        </div>
+                        
+                        <div class="report-buttons">
+                            {{#comments}}<i title="{{comments}} Commentaire(s)" class="fas fa-comment text-muted btn-comments"></i> {{comments}}{{/comments}}
+
+                            <i class="far fa-trash-alt pointer btn-delete text-muted"  title="Supprimer le rapport" onclick="issue_issuereport_delete(this);"></i>
+                        </div>
+                    </li>
+              
+            </ul>
+
+             <!-- 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');issue_issuereport_search();">
+                    <a class="page-link" href="#">{{label}}</a>
+                </li>
+            </ul>
+        </div>
+
+        <!-- Onglet Paramètres -->
+        <div class="tab-pane" id="tab-settings" role="tabpanel" aria-labelledby="tab-settings">
+            <div class="row">
+                <div class="col-md-12">
+                    <div onclick="issue_setting_save();" class="btn btn-success float-right"><i class="fas fa-check"></i> Enregistrer</div>
+                    <legend>Paramètres généraux :</legend>
+                    <div class="clear"></div>
+                </div>
+            </div>
+            <?php echo Configuration::html('issue'); ?>
+        </div>
+    </div>
+</div>

+ 22 - 0
plugin/notification/action.php

@@ -226,6 +226,28 @@ switch($_['action']){
 			$myUser->preference('notification_categories',json_encode($preferences));
 		});
 	break;
+
+
+	//Sauvegarde des configurations de
+	case 'notification_setting_save':
+		Action::write(function(&$response){
+			global $myUser,$_,$conf;
+			User::check_access('notification','configure');
+			foreach(Configuration::setting('notification') as $key=>$value){
+				if(!is_array($value)) continue;
+				$allowed[] = $key;
+			}
+			foreach ($_['fields'] as $key => $value){
+				if(!in_array($key, $allowed)) continue;
+				
+				$value = html_entity_decode($value);
+				if($key=='notification_mail_reply' && !check_mail($value)) throw new Exception("Email invalide :".$value);
+				
+				$conf->put($key,$value);
+			}
+		});
+	break;
+
 }
 
 

+ 11 - 0
plugin/notification/js/main.js

@@ -409,3 +409,14 @@ function notification_send(){
 	});
 }
 
+
+
+function notification_setting_save(){
+
+	$.action({ 
+		action : 'notification_setting_save', 
+		fields : $('#notification-setting-form').toJson() 
+	},function(){
+		$.message('success','Enregistré');
+	});
+}

+ 14 - 1
plugin/notification/notification.plugin.php

@@ -140,8 +140,14 @@ function notification_methods(&$sendTypes){
 			if(!isset($recipient)) throw new Exception("Aucun destinataires n\'a été renseignés pour l\'envoi de mail");
 
 			// ENVOI MAIL
+
+			global $conf;
+			$expeditor = $conf->get('notification_mail_from') != '' ? $conf->get('notification_mail_from'): 'Hackpoint <no-reply@hackpoint.fr>';
+			$reply = $conf->get('notification_mail_reply') != '' ? $conf->get('notification_mail_reply'): 'no-reply@hackpoint.fr';
+
 			$mail = new Mail();
-			$mail->expeditor = 'Sys1 <no-reply@idleman.fr>';
+			$mail->expeditor = $expeditor;
+			$mail->reply = $reply;
 			$mail->title = $infos['label'];
 			$mail->message = '<h3>'.$infos['label'].'</h3><p>'.$infos['html'].'</p>';
 			$mail->message .= (isset($infos['meta']['link'])) ? '<a href="'.$infos['meta']['link'].'">Accéder à l\'ERP</a>' : '';
@@ -197,6 +203,13 @@ function notification_widget(&$widgets){
 	$widgets[] = $modelWidget;
 }
 
+//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('notification',array(
+    "E-mail",
+    'notification_mail_from' => array("label"=>"Envois de la part de","legend"=>"format: Nom &lt;email&gt;, &nbsp;&nbsp;&nbsp; ex: Idleman &lt;no-reply@hackpoint.fr&gt;","type"=>"text"),
+    'notification_mail_reply' => array("label"=>"Réponse au mail","legend"=>"adresse e-mail unique ex: me@domain.com","type"=>"text"),
+));
 
 //Déclation des assets
 Plugin::addCss("/css/main.css"); 

+ 16 - 3
plugin/notification/setting.notification.php

@@ -5,16 +5,29 @@ require_once(__DIR__.SLASH.'Notification.class.php');
 User::check_access('notification','configure');
 $types = array();
 ?>
+
+<div class="row">
+	<div class="col-md-12">
+				<br>
+		        <div onclick="notification_setting_save();" class="btn btn-success right"><i class="fas fa-check"></i> Enregistrer</div>
+		        <h3>Réglages Notifications</h3>
+		        <hr>
+	</div>
+</div>
 <div class="row">
 	<div class="col-md-12" id="notification_preference">
 		<br>
-		<h3>Notifications</h3>
+
+		
+		<h3>Options générales</h3>
+		<?php echo Configuration::html('notification'); ?>
+	
+
+		<h3>Envois de notifications</h3>
 		<div class="clear"></div>
 		<hr>
 		<p>Dans cette section, vous pouvez créer une notification pour les utilisateurs et types concernés</p>
 		
-
-
 		<div class="row">
 			<div class="col-md-6">
 				<label for="recipients">Destinataires  </label> <small class="text-muted"> - Laisser vide pour envoyer à tout le monde</small>

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