Edgard 5 лет назад
Родитель
Сommit
f0cb609a17

+ 1 - 1
composer.lock

@@ -358,5 +358,5 @@
     "platform-overrides": {
         "php": "7.1.2"
     },
-    "plugin-api-version": "1.1.0"
+    "plugin-api-version": "2.0.0"
 }

+ 14 - 5
front/callback.php

@@ -7,9 +7,9 @@ ini_set('display_errors', 1);
 ini_set('display_startup_errors', 1);
 error_reporting(E_ALL);
 
-include ('../../../inc/includes.php');
+include('../../../inc/includes.php');
 
-$provider_id = PluginSinglesignonProvider::getCallbackParameters('provider');
+$provider_id = PluginSinglesignonToolbox::getCallbackParameters('provider');
 
 if (!$provider_id) {
    Html::displayErrorAndDie(__sso("Provider not defined."), false);
@@ -27,24 +27,33 @@ if (!$signon_provider->fields['is_active']) {
 
 $signon_provider->checkAuthorization();
 
-$test = PluginSinglesignonProvider::getCallbackParameters('test');
+$test = PluginSinglesignonToolbox::getCallbackParameters('test');
 
 if ($test) {
    Html::nullHeader("Login", $CFG_GLPI["root_doc"] . '/index.php');
    echo '<div class="left spaced">';
    echo '<pre>';
+   echo "### BEGIN ###\n";
    print_r($signon_provider->getResourceOwner());
+   echo "### END ###";
    echo '</pre>';
    Html::nullFooter();
    exit();
 }
 
+$user_id = Session::getLoginUserID();
 
 $REDIRECT = "";
 
-if ($signon_provider->login()) {
+if ($user_id || $signon_provider->login()) {
 
-   $params = PluginSinglesignonProvider::getCallbackParameters('q');
+   $user_id = $user_id ?: Session::getLoginUserID();
+
+   if ($user_id) {
+      $signon_provider->linkUser($user_id);
+   }
+
+   $params = PluginSinglesignonToolbox::getCallbackParameters('q');
 
    $url_redirect = '';
 

+ 1 - 1
front/picture.send.php

@@ -22,4 +22,4 @@ if (!file_exists($path)) {
    Html::displayErrorAndDie(__('File not found'), true); // Not found
 }
 
-Toolbox::sendFile($path, $logo);
+Toolbox::sendFile($path, "logo.png");

+ 17 - 0
front/preference.form.php

@@ -0,0 +1,17 @@
+<?php
+
+include ('../../../inc/includes.php');
+
+Session::checkLoginUser();
+
+if (isset($_POST["update"])) {
+
+   $prefer = new PluginSinglesignonPreference(Session::getLoginUserID());
+   $prefer->loadProviders();
+
+   $prefer->update($_POST);
+
+   Html::back();
+} else {
+   Html::back();
+}

+ 17 - 0
front/user.form.php

@@ -0,0 +1,17 @@
+<?php
+
+include('../../../inc/includes.php');
+
+Session::checkRight(User::$rightname, UPDATE);
+
+if (isset($_POST["update"]) && isset($_POST["user_id"])) {
+
+   $prefer = new PluginSinglesignonPreference((int) $_POST["user_id"]);
+   $prefer->loadProviders();
+
+   $prefer->update($_POST);
+
+   Html::back();
+} else {
+   Html::back();
+}

+ 15 - 3
hook.php

@@ -20,14 +20,14 @@ function plugin_singlesignon_display_login() {
          $query['redirect'] = $_REQUEST['redirect'];
       }
 
-      $url = PluginSinglesignonProvider::getCallbackUrl($row['id'], $query);
-      $html[] = PluginSinglesignonProvider::renderButton($url, $row);
+      $url = PluginSinglesignonToolbox::getCallbackUrl($row['id'], $query);
+      $html[] = PluginSinglesignonToolbox::renderButton($url, $row);
    }
 
    if (!empty($html)) {
       echo '<div class="singlesignon-box">';
       echo implode(" \n", $html);
-      echo PluginSinglesignonProvider::renderButton('#', ['name' => __('GLPI')], 'vsubmit old-login');
+      echo PluginSinglesignonToolbox::renderButton('#', ['name' => __('GLPI')], 'vsubmit old-login');
       echo '</div>';
       ?>
       <style>
@@ -181,6 +181,18 @@ function plugin_singlesignon_install() {
                 ADD `color` varchar(7) DEFAULT NULL";
       $DB->query($query) or die("error adding picture column " . $DB->error());
    }
+   if (version_compare($currentVersion, "1.3.0", '<')) {
+      $query = "CREATE TABLE `glpi_plugin_singlesignon_providers_users` (
+         `id` int(11) NOT NULL AUTO_INCREMENT,
+         `plugin_singlesignon_providers_id` int(11) NOT NULL DEFAULT '0',
+         `users_id` int(11) NOT NULL DEFAULT '0',
+         `remote_id` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
+         PRIMARY KEY (`id`),
+         UNIQUE KEY `unicity` (`plugin_singlesignon_providers_id`,`users_id`),
+         UNIQUE KEY `unicity_remote` (`plugin_singlesignon_providers_id`,`remote_id`)
+       ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;";
+      $DB->query($query) or die("error creating glpi_plugin_singlesignon_providers_users " . $DB->error());
+   }
 
    Config::setConfigurationValues('singlesignon', [
       'version' => PLUGIN_SINGLESIGNON_VERSION,

+ 211 - 0
inc/preference.class.php

@@ -0,0 +1,211 @@
+<?php
+
+class PluginSinglesignonPreference extends CommonDBTM {
+
+   static protected $notable = true;
+   static $rightname = '';
+
+   // Provider data
+   public $user_id = null;
+   public $providers = [];
+   public $providers_users = [];
+
+   public function __construct($user_id = null) {
+      parent::__construct();
+
+      $this->user_id = $user_id;
+   }
+
+   public function loadProviders() {
+      $signon_provider = new PluginSinglesignonProvider();
+
+      $condition = '`is_active` = 1';
+      if (version_compare(GLPI_VERSION, '9.4', '>=')) {
+         $condition = [$condition];
+      }
+      $this->providers = $signon_provider->find($condition);
+
+      $provider_user = new PluginSinglesignonProvider_User();
+
+      $condition = "`users_id` = {$this->user_id}";
+      if (version_compare(GLPI_VERSION, '9.4', '>=')) {
+         $condition = [$condition];
+      }
+      $this->providers_users = $provider_user->find($condition);
+   }
+
+   public function update(array $input, $history = 1, $options = []) {
+      if (!isset($input['_remove_sso']) || !is_array($input['_remove_sso'])) {
+         return false;
+      }
+
+      $ids = $input['_remove_sso'];
+      if (empty($ids)) {
+         return false;
+      }
+
+      $provider_user = new PluginSinglesignonProvider_User();
+      $condition = "`users_id` = {$this->user_id} AND `id` IN (" . implode(',', $ids) . ")";
+      if (version_compare(GLPI_VERSION, '9.4', '>=')) {
+         $condition = [$condition];
+      }
+
+      $providers_users = $provider_user->find($condition);
+
+      foreach ($providers_users as $pu) {
+         $provider_user->delete($pu);
+      }
+   }
+
+   function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) {
+      switch (get_class($item)) {
+         case 'Preference':
+         case 'User':
+            return [1 => __sso('Single Sign-on')];
+         default:
+            return '';
+      }
+   }
+
+   static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) {
+      switch (get_class($item)) {
+         case 'User':
+            $prefer = new self($item->fields['id']);
+            $prefer->loadProviders();
+            $prefer->showFormUser($item);
+            break;
+         case 'Preference':
+            $prefer = new self(Session::getLoginUserID());
+            $prefer->loadProviders();
+            $prefer->showFormPreference($item);
+            break;
+      }
+      return true;
+   }
+
+   function showFormUser(CommonGLPI $item) {
+      global $CFG_GLPI;
+
+      if (!User::canView()) {
+         return false;
+      }
+      $canedit = Session::haveRight(User::$rightname, UPDATE);
+      if ($canedit) {
+         echo "<form name='form' action=\"" . $CFG_GLPI['root_doc'] . "/plugins/singlesignon/front/user.form.php\" method='post'>";
+      }
+      echo Html::hidden('user_id', ['value' => $this->user_id]);
+
+      echo "<div class='center' id='tabsbody'>";
+      echo "<table class='tab_cadre_fixe'>";
+
+      echo "<tr><th colspan='4'>" . __('Settings') . "</th></tr>";
+
+      $this->showFormDefault($item);
+
+      if ($canedit) {
+         echo "<tr class='tab_bg_2'>";
+         echo "<td colspan='4' class='center'>";
+         echo "<input type='submit' name='update' class='submit' value=\"" . _sx('button', 'Save') . "\">";
+         echo "</td></tr>";
+      }
+
+      echo "</table></div>";
+      Html::closeForm();
+   }
+
+   function showFormPreference(CommonGLPI $item) {
+      $user = new User();
+      if (!$user->can($this->user_id, READ) && ($this->user_id != Session::getLoginUserID())) {
+         return false;
+      }
+      $canedit = $this->user_id == Session::getLoginUserID();
+
+      if ($canedit) {
+         echo "<form name='form' action=\"" . Toolbox::getItemTypeFormURL(__CLASS__) . "\" method='post'>";
+      }
+
+      echo "<div class='center' id='tabsbody'>";
+      echo "<table class='tab_cadre_fixe'>";
+
+      echo "<tr><th colspan='4'>" . __('Settings') . "</th></tr>";
+
+      $this->showFormDefault($item);
+
+      if ($canedit) {
+         echo "<tr class='tab_bg_2'>";
+         echo "<td colspan='4' class='center'>";
+         echo "<input type='submit' name='update' class='submit' value=\"" . _sx('button', 'Save') . "\">";
+         echo "</td></tr>";
+      }
+
+      echo "</table></div>";
+      Html::closeForm();
+   }
+
+   function showFormDefault(CommonGLPI $item) {
+      echo "<tr class='tab_bg_2'>";
+      echo "<td> " . __sso('Single Sign-on Provider') . "</td><td>";
+
+      foreach ($this->providers as $p) {
+         switch (get_class($item)) {
+            case 'User':
+               $redirect = $item->getFormURLWithID($this->user_id, true);
+               break;
+            case 'Preference':
+               $redirect = $item->getSearchURL(false);
+               break;
+            default:
+               $redirect = '';
+         }
+
+         $url = PluginSinglesignonToolbox::getCallbackUrl($p['id'], ['redirect' => $redirect]);
+
+         echo PluginSinglesignonToolbox::renderButton($url, $p);
+         echo " ";
+      }
+
+      echo "</td></tr>";
+
+      echo "<tr class='tab_bg_2'>";
+
+      if (!empty($this->providers_users)) {
+         echo "<tr><th colspan='2'>" . __sso('Linked accounts') . "</th></tr>";
+
+         foreach ($this->providers_users as $pu) {
+            /** @var PluginSinglesignonProvider */
+            $provider = PluginSinglesignonProvider::getById($pu['plugin_singlesignon_providers_id']);
+
+            echo "<tr><td>";
+            echo $provider->fields['name'] . ' (ID:' . $pu['remote_id'] . ')';
+            echo "</td><td>";
+            echo Html::getCheckbox([
+               'title' => __('Clear'),
+               'name'  => "_remove_sso[]",
+               'value'  => $pu['id'],
+            ]);
+            echo "&nbsp;" . __('Clear');
+            echo "</td></tr>";
+         }
+      }
+
+      ?>
+      <script type="text/javascript">
+         $(document).ready(function() {
+
+            // On click, open a popup
+            $(document).on("click", ".singlesignon.oauth-login", function(e) {
+               e.preventDefault();
+
+               var url = $(this).attr("href");
+               var left = ($(window).width() / 2) - (600 / 2);
+               var top = ($(window).height() / 2) - (800 / 2);
+               var newWindow = window.open(url, "singlesignon", "width=600,height=800,left=" + left + ",top=" + top);
+               if (window.focus) {
+                  newWindow.focus();
+               }
+            });
+         });
+      </script>
+      <?php
+   }
+}

+ 65 - 145
inc/provider.class.php

@@ -180,7 +180,7 @@ class PluginSinglesignonProvider extends CommonDBTM {
       echo "<td>" . __('Picture') . "</td>";
       echo "<td colspan='3'>";
       if (!empty($this->fields['picture'])) {
-         echo Html::image(static::getPictureUrl($this->fields['picture']), [
+         echo Html::image(PluginSinglesignonToolbox::getPictureUrl($this->fields['picture']), [
             'style' => '
                max-width: 100px;
                max-height: 100px;
@@ -218,7 +218,7 @@ class PluginSinglesignonProvider extends CommonDBTM {
          echo "<th colspan='4'>" . __('Test') . "</th>";
          echo "</tr>\n";
 
-         $url = self::getCallbackUrl($ID);
+         $url = PluginSinglesignonToolbox::getCallbackUrl($ID);
          $fullUrl = $this->getBaseURL() . $url;
          echo "<tr class='tab_bg_1'>";
          echo "<td>" . __sso('Callback URL') . "</td>";
@@ -258,7 +258,12 @@ class PluginSinglesignonProvider extends CommonDBTM {
    }
 
    function cleanDBonPurge() {
-      static::deletePicture($this->fields['picture']);
+      PluginSinglesignonToolbox::deletePicture($this->fields['picture']);
+      $this->deleteChildrenAndRelationsFromDb(
+         [
+            'PluginSinglesignonProvider_User',
+         ]
+      );
    }
 
    /**
@@ -344,21 +349,21 @@ class PluginSinglesignonProvider extends CommonDBTM {
          $input['picture'] = '';
 
          if (array_key_exists('picture', $this->fields)) {
-            static::deletePicture($this->fields['picture']);
+            PluginSinglesignonToolbox::deletePicture($this->fields['picture']);
          }
       }
 
       if (isset($input["_picture"])) {
          $picture = array_shift($input["_picture"]);
 
-         if ($dest = static::savePicture(GLPI_TMP_DIR . '/' . $picture)) {
+         if ($dest = PluginSinglesignonToolbox::savePicture(GLPI_TMP_DIR . '/' . $picture)) {
             $input['picture'] = $dest;
          } else {
             Session::addMessageAfterRedirect(__('Unable to save picture file.'), true, ERROR);
          }
 
          if (array_key_exists('picture', $this->fields)) {
-            static::deletePicture($this->fields['picture']);
+            PluginSinglesignonToolbox::deletePicture($this->fields['picture']);
          }
       }
 
@@ -1021,6 +1026,34 @@ class PluginSinglesignonProvider extends CommonDBTM {
          return $user;
       }
 
+      $remote_id = false;
+      $remote_id_fields = ['id', 'username'];
+
+      foreach ($remote_id_fields as $field) {
+         if (isset($resource_array[$field]) && !empty($resource_array[$field])) {
+            $remote_id = $resource_array[$field];
+            break;
+         }
+      }
+
+      if ($remote_id) {
+         $link = new PluginSinglesignonProvider_User();
+         $condition = "`remote_id` = '{$remote_id}' AND `plugin_singlesignon_providers_id` = {$this->fields['id']}";
+         if (version_compare(GLPI_VERSION, '9.4', '>=')) {
+            $condition = [$condition];
+         }
+         $links = $link->find($condition);
+         if (!empty($links) && $first = reset($links)) {
+            $id = $first['users_id'];
+         }
+
+         $remote_id;
+      }
+
+      if (is_numeric($id) && $user->getFromDB($id)) {
+         return $user;
+      }
+
       $email = false;
       $email_fields = ['email', 'e-mail', 'email-address', 'mail'];
 
@@ -1078,158 +1111,45 @@ class PluginSinglesignonProvider extends CommonDBTM {
       return $auth->auth_succeded;
    }
 
-   /**
-    * Generate a URL to callback
-    * Some providers don't accept query string, it convert to PATH
-    * @global array $CFG_GLPI
-    * @param integer $id
-    * @param array $query
-    * @return string
-    */
-   public static function getCallbackUrl($id, $query = []) {
-      global $CFG_GLPI;
-
-      $url = $CFG_GLPI['root_doc'] . '/plugins/singlesignon/front/callback.php';
-
-      $url .= "/provider/$id";
-
-      if (!empty($query)) {
-         $url .= "/q/" . base64_encode(http_build_query($query));
-      }
-
-      return $url;
-   }
-
-   public static function getCallbackParameters($name = null) {
-      $data = [];
-
-      if (isset($_SERVER['PATH_INFO'])) {
-         $path_info = trim($_SERVER['PATH_INFO'], '/');
-
-         $parts = explode('/', $path_info);
-
-         $key = null;
-
-         foreach ($parts as $part) {
-            if ($key === null) {
-               $key = $part;
-            } else {
-               if ($key === "provider" || $key === "test") {
-                  $part = intval($part);
-               } else {
-                  $tmp = base64_decode($part);
-                  parse_str($tmp, $part);
-               }
-
-               if ($key === $name) {
-                  return $part;
-               }
-
-               $data[$key] = $part;
-               $key = null;
-            }
-         }
-      }
-
-      if (!isset($data[$name])) {
-         return null;
-      }
-
-      return $data;
-   }
-
-   static public function startsWith($haystack, $needle) {
-      $length = strlen($needle);
-      return (substr($haystack, 0, $length) === $needle);
-   }
-
-   static function getPictureUrl($path) {
-      global $CFG_GLPI;
-
-      $path = Html::cleanInputText($path); // prevent xss
-
-      if (empty($path)) {
-         return null;
-      }
-
-      return $CFG_GLPI['root_doc'] . '/plugins/singlesignon/front/picture.send.php?path=' . $path;
-   }
-
-   static public function savePicture($src, $uniq_prefix = null) {
-
-      if (function_exists('Document::isImage') && !Document::isImage($src)) {
-         return false;
+   public function linkUser($user_id) {
+      /** @var User */
+      $user = User::getById($user_id);
+      if (!$user) {
+         return;
       }
 
-      $filename     = uniqid($uniq_prefix);
-      $ext          = pathinfo($src, PATHINFO_EXTENSION);
-      $subdirectory = substr($filename, -2); // subdirectory based on last 2 hex digit
-
-      $basePath = GLPI_PLUGIN_DOC_DIR . "/singlesignon";
-      $i = 0;
-      do {
-         // Iterate on possible suffix while dest exists.
-         // This case will almost never exists as dest is based on an unique id.
-         $dest = $basePath
-         . '/' . $subdirectory
-         . '/' . $filename . ($i > 0 ? '_' . $i : '') . '.' . $ext;
-         $i++;
-      } while (file_exists($dest));
-
-      if (!is_dir($basePath . '/' . $subdirectory) && !mkdir($basePath . '/' . $subdirectory)) {
-         return false;
-      }
+      $resource_array = $this->getResourceOwner();
 
-      if (!rename($src, $dest)) {
+      if (!$resource_array) {
          return false;
       }
 
-      return substr($dest, strlen($basePath . '/')); // Return dest relative to GLPI_PICTURE_DIR
-   }
-
-   public static function deletePicture($path) {
-      $basePath = GLPI_PLUGIN_DOC_DIR . "/singlesignon";
-      $fullpath = $basePath . '/' . $path;
+      $remote_id = false;
+      $id_fields = ['id', 'sub', 'username'];
 
-      if (!file_exists($fullpath)) {
-         return false;
+      foreach ($id_fields as $field) {
+         if (isset($resource_array[$field]) && !empty($resource_array[$field])) {
+            $remote_id = $resource_array[$field];
+            break;
+         }
       }
 
-      $fullpath = realpath($fullpath);
-      if (!static::startsWith($fullpath, realpath($basePath))) {
+      if (!$remote_id) {
          return false;
       }
 
-      return @unlink($fullpath);
-   }
-
-   public static function renderButton($url, $data, $class = 'oauth-login') {
-      $btn = '<span><a href="' . $url . '" class="singlesignon vsubmit ' . $class . '"';
+      $link = new PluginSinglesignonProvider_User();
 
-      $style = '';
-      if ((isset($data['bgcolor']) && $data['bgcolor'])) {
-         $style .= 'background-color: ' . $data['bgcolor'] . ';';
-      }
-      if ((isset($data['color']) && $data['color'])) {
-         $style .= 'color: ' . $data['color'] . ';';
-      }
-      if ($style) {
-         $btn .= ' style="' . $style . '"';
-      }
-      $btn .= '>';
-
-      if (isset($data['picture']) && $data['picture']) {
-         $btn .= Html::image(
-            static::getPictureUrl($data['picture']),
-            [
-               'style' => 'max-height: 20px;',
-            ]
-         );
-         $btn .= ' ';
-      }
+      // Unlink from another user
+      $link->deleteByCriteria([
+         'plugin_singlesignon_providers_id' => $this->fields['id'],
+         'remote_id' => $remote_id,
+      ]);
 
-      $btn .= sprintf(__sso('Login with %s'), $data['name']);
-      $btn .= '</a></span>';
-      return $btn;
+      return $link->add([
+         'plugin_singlesignon_providers_id' => $this->fields['id'],
+         'users_id' => $user_id,
+         'remote_id' => $remote_id,
+      ]);
    }
 }

+ 11 - 0
inc/provider_user.class.php

@@ -0,0 +1,11 @@
+<?php
+
+class PluginSinglesignonProvider_User extends CommonDBRelation {
+
+   // From CommonDBRelation
+   static public $itemtype_1   = 'PluginSinglesignonProvider';
+   static public $items_id_1   = 'plugin_singlesignon_providers_id';
+
+   static public $itemtype_2 = 'User';
+   static public $items_id_2 = 'users_id';
+}

+ 158 - 0
inc/toolbox.class.php

@@ -0,0 +1,158 @@
+<?php
+
+class PluginSinglesignonToolbox {
+   /**
+    * Generate a URL to callback
+    * Some providers don't accept query string, it convert to PATH
+    * @global array $CFG_GLPI
+    * @param integer $id
+    * @param array $query
+    * @return string
+    */
+   public static function getCallbackUrl($id, $query = []) {
+      global $CFG_GLPI;
+
+      $url = $CFG_GLPI['root_doc'] . '/plugins/singlesignon/front/callback.php';
+
+      $url .= "/provider/$id";
+
+      if (!empty($query)) {
+         $url .= "/q/" . base64_encode(http_build_query($query));
+      }
+
+      return $url;
+   }
+
+   public static function getCallbackParameters($name = null) {
+      $data = [];
+
+      if (isset($_SERVER['PATH_INFO'])) {
+         $path_info = trim($_SERVER['PATH_INFO'], '/');
+
+         $parts = explode('/', $path_info);
+
+         $key = null;
+
+         foreach ($parts as $part) {
+            if ($key === null) {
+               $key = $part;
+            } else {
+               if ($key === "provider" || $key === "test") {
+                  $part = intval($part);
+               } else {
+                  $tmp = base64_decode($part);
+                  parse_str($tmp, $part);
+               }
+
+               if ($key === $name) {
+                  return $part;
+               }
+
+               $data[$key] = $part;
+               $key = null;
+            }
+         }
+      }
+
+      if (!isset($data[$name])) {
+         return null;
+      }
+
+      return $data;
+   }
+
+   static public function startsWith($haystack, $needle) {
+      $length = strlen($needle);
+      return (substr($haystack, 0, $length) === $needle);
+   }
+
+   static function getPictureUrl($path) {
+      global $CFG_GLPI;
+
+      $path = Html::cleanInputText($path); // prevent xss
+
+      if (empty($path)) {
+         return null;
+      }
+
+      return $CFG_GLPI['root_doc'] . '/plugins/singlesignon/front/picture.send.php?path=' . $path;
+   }
+
+   static public function savePicture($src, $uniq_prefix = null) {
+
+      if (function_exists('Document::isImage') && !Document::isImage($src)) {
+         return false;
+      }
+
+      $filename     = uniqid($uniq_prefix);
+      $ext          = pathinfo($src, PATHINFO_EXTENSION);
+      $subdirectory = substr($filename, -2); // subdirectory based on last 2 hex digit
+
+      $basePath = GLPI_PLUGIN_DOC_DIR . "/singlesignon";
+      $i = 0;
+      do {
+         // Iterate on possible suffix while dest exists.
+         // This case will almost never exists as dest is based on an unique id.
+         $dest = $basePath
+         . '/' . $subdirectory
+         . '/' . $filename . ($i > 0 ? '_' . $i : '') . '.' . $ext;
+         $i++;
+      } while (file_exists($dest));
+
+      if (!is_dir($basePath . '/' . $subdirectory) && !mkdir($basePath . '/' . $subdirectory)) {
+         return false;
+      }
+
+      if (!rename($src, $dest)) {
+         return false;
+      }
+
+      return substr($dest, strlen($basePath . '/')); // Return dest relative to GLPI_PICTURE_DIR
+   }
+
+   public static function deletePicture($path) {
+      $basePath = GLPI_PLUGIN_DOC_DIR . "/singlesignon";
+      $fullpath = $basePath . '/' . $path;
+
+      if (!file_exists($fullpath)) {
+         return false;
+      }
+
+      $fullpath = realpath($fullpath);
+      if (!static::startsWith($fullpath, realpath($basePath))) {
+         return false;
+      }
+
+      return @unlink($fullpath);
+   }
+
+   public static function renderButton($url, $data, $class = 'oauth-login') {
+      $btn = '<span><a href="' . $url . '" class="singlesignon vsubmit ' . $class . '"';
+
+      $style = '';
+      if ((isset($data['bgcolor']) && $data['bgcolor'])) {
+         $style .= 'background-color: ' . $data['bgcolor'] . ';';
+      }
+      if ((isset($data['color']) && $data['color'])) {
+         $style .= 'color: ' . $data['color'] . ';';
+      }
+      if ($style) {
+         $btn .= ' style="' . $style . '"';
+      }
+      $btn .= '>';
+
+      if (isset($data['picture']) && $data['picture']) {
+         $btn .= Html::image(
+            static::getPictureUrl($data['picture']),
+            [
+               'style' => 'max-height: 20px;',
+            ]
+         );
+         $btn .= ' ';
+      }
+
+      $btn .= sprintf(__sso('Login with %s'), $data['name']);
+      $btn .= '</a></span>';
+      return $btn;
+   }
+}

+ 42 - 37
locales/en_GB.po

@@ -5,11 +5,11 @@
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: singlesignon 1.0.0\n"
+"Project-Id-Version: singlesignon 1.3.0\n"
 "Report-Msgid-Bugs-To: https://github.com/edgardmessias/glpi-singlesignon/"
 "issues\n"
-"POT-Creation-Date: 2021-01-12 13:25-0300\n"
-"PO-Revision-Date: 2021-01-12 13:25-0300\n"
+"POT-Creation-Date: 2021-01-20 14:48-0300\n"
+"PO-Revision-Date: 2021-01-20 14:48-0300\n"
 "Last-Translator: Automatically generated\n"
 "Language-Team: none\n"
 "Language: en_GB\n"
@@ -18,22 +18,18 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: hook.php:27 hook.php:75
-#, php-format
-msgid "Login with %s"
-msgstr "Login with %s"
-
 #: setup.php:8
 #, php-format
 msgid "Please, rename the plugin folder \"%s\" to \"singlesignon\""
 msgstr "Please, rename the plugin folder \"%s\" to \"singlesignon\""
 
-#: setup.php:36 front/provider.form.php:59 front/provider.form.php:61
-#: front/provider.php:6 front/provider.php:8 inc/provider.class.php:57
+#: setup.php:40 front/provider.form.php:59 front/provider.form.php:61
+#: front/provider.php:6 front/provider.php:8 inc/preference.class.php:64
+#: inc/provider.class.php:57
 msgid "Single Sign-on"
 msgstr "Single Sign-on"
 
-#: setup.php:53
+#: setup.php:57
 msgid "This plugin requires GLPI >= 0.85"
 msgstr "This plugin requires GLPI >= 0.85"
 
@@ -49,111 +45,120 @@ msgstr "Provider not found."
 msgid "Provider not active."
 msgstr "Provider not active."
 
-#: inc/provider.class.php:50
+#: inc/preference.class.php:147 inc/provider.class.php:50
 msgid "Single Sign-on Provider"
 msgstr "Single Sign-on Provider"
 
+#: inc/preference.class.php:172
+msgid "Linked accounts"
+msgstr "Linked accounts"
+
 #: inc/provider.class.php:98
 msgid "SSO Type"
 msgstr "SSO Type"
 
-#: inc/provider.class.php:106 inc/provider.class.php:298
+#: inc/provider.class.php:106 inc/provider.class.php:426
 msgid "Client ID"
 msgstr "Client ID"
 
-#: inc/provider.class.php:108 inc/provider.class.php:306
+#: inc/provider.class.php:108 inc/provider.class.php:434
 msgid "Client Secret"
 msgstr "Client Secret"
 
-#: inc/provider.class.php:113 inc/provider.class.php:314
+#: inc/provider.class.php:113 inc/provider.class.php:442
 msgid "Scope"
 msgstr "Scope"
 
-#: inc/provider.class.php:115 inc/provider.class.php:322
+#: inc/provider.class.php:115 inc/provider.class.php:450
 msgid "Extra Options"
 msgstr "Extra Options"
 
-#: inc/provider.class.php:126 inc/provider.class.php:330
+#: inc/provider.class.php:126 inc/provider.class.php:458
 msgid "Authorize URL"
 msgstr "Authorize URL"
 
-#: inc/provider.class.php:131 inc/provider.class.php:338
+#: inc/provider.class.php:131 inc/provider.class.php:466
 msgid "Access Token URL"
 msgstr "Access Token URL"
 
-#: inc/provider.class.php:136 inc/provider.class.php:346
+#: inc/provider.class.php:136 inc/provider.class.php:474
 msgid "Resource Owner Details URL"
 msgstr "Resource Owner Details URL"
 
-#: inc/provider.class.php:144
+#: inc/provider.class.php:224
 msgid "Callback URL"
 msgstr "Callback URL"
 
-#: inc/provider.class.php:148
+#: inc/provider.class.php:228
 msgid "Test Single Sign-on"
 msgstr "Test Single Sign-on"
 
-#: inc/provider.class.php:197
+#: inc/provider.class.php:286
 msgid "A Name is required"
 msgstr "A Name is required"
 
-#: inc/provider.class.php:203
+#: inc/provider.class.php:292
 #, php-format
 msgid "The \"%s\" is a Invalid type"
 msgstr "The \"%s\" is a Invalid type"
 
-#: inc/provider.class.php:207
+#: inc/provider.class.php:296
 msgid "A Client ID is required"
 msgstr "A Client ID is required"
 
-#: inc/provider.class.php:211
+#: inc/provider.class.php:300
 msgid "A Client Secret is required"
 msgstr "A Client Secret is required"
 
-#: inc/provider.class.php:216
+#: inc/provider.class.php:305
 msgid "An Authorize URL is required"
 msgstr "An Authorize URL is required"
 
-#: inc/provider.class.php:218
+#: inc/provider.class.php:307
 msgid "The Authorize URL is invalid"
 msgstr "The Authorize URL is invalid"
 
-#: inc/provider.class.php:222
+#: inc/provider.class.php:311
 msgid "An Access Token URL is required"
 msgstr "An Access Token URL is required"
 
-#: inc/provider.class.php:224
+#: inc/provider.class.php:313
 msgid "The Access Token URL is invalid"
 msgstr "The Access Token URL is invalid"
 
-#: inc/provider.class.php:228
+#: inc/provider.class.php:317
 msgid "A Resource Owner Details URL is required"
 msgstr "A Resource Owner Details URL is required"
 
-#: inc/provider.class.php:230
+#: inc/provider.class.php:319
 msgid "The Resource Owner Details URL is invalid"
 msgstr "The Resource Owner Details URL is invalid"
 
-#: inc/provider.class.php:405
+#: inc/provider.class.php:533
 msgid "Generic"
 msgstr "Generic"
 
-#: inc/provider.class.php:406
+#: inc/provider.class.php:534
 msgid "Facebook"
 msgstr "Facebook"
 
-#: inc/provider.class.php:407
+#: inc/provider.class.php:535
 msgid "GitHub"
 msgstr "GitHub"
 
-#: inc/provider.class.php:408
+#: inc/provider.class.php:536
 msgid "Google"
 msgstr "Google"
 
-#: inc/provider.class.php:409
+#: inc/provider.class.php:537
 msgid "Instagram"
 msgstr "Instagram"
 
-#: inc/provider.class.php:410
+#: inc/provider.class.php:538
 msgid "LinkdeIn"
 msgstr "LinkdeIn"
+
+#: inc/toolbox.class.php:154
+#, php-format
+msgid "Login with %s"
+msgstr "Login with %s"

+ 45 - 41
locales/pt_BR.po

@@ -8,7 +8,7 @@ msgstr ""
 "Project-Id-Version: singlesignon 1.0.0\n"
 "Report-Msgid-Bugs-To: https://github.com/edgardmessias/glpi-singlesignon/"
 "issues\n"
-"POT-Creation-Date: 2021-01-12 13:25-0300\n"
+"POT-Creation-Date: 2021-01-20 14:48-0300\n"
 "PO-Revision-Date: 2019-04-26 11:04-0300\n"
 "Last-Translator: Edgard Lorraine Messias <edgardmessias@gmail.com>\n"
 "Language-Team: none\n"
@@ -18,22 +18,18 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
-#: hook.php:27 hook.php:75
-#, php-format
-msgid "Login with %s"
-msgstr "Entrar com %s"
-
 #: setup.php:8
 #, php-format
 msgid "Please, rename the plugin folder \"%s\" to \"singlesignon\""
 msgstr "Por favor, renomeie a pasta do plugin \"%s\" para \"singlesignon\""
 
-#: setup.php:36 front/provider.form.php:59 front/provider.form.php:61
-#: front/provider.php:6 front/provider.php:8 inc/provider.class.php:57
+#: setup.php:40 front/provider.form.php:59 front/provider.form.php:61
+#: front/provider.php:6 front/provider.php:8 inc/preference.class.php:64
+#: inc/provider.class.php:57
 msgid "Single Sign-on"
-msgstr "Single Sign-on"
+msgstr "Logon Único"
 
-#: setup.php:53
+#: setup.php:57
 msgid "This plugin requires GLPI >= 0.85"
 msgstr "Este plugin requer GLPI >= 0.85"
 
@@ -49,115 +45,123 @@ msgstr "Provedor não encontrado."
 msgid "Provider not active."
 msgstr "Provedor não ativo."
 
-#: inc/provider.class.php:50
+#: inc/preference.class.php:147 inc/provider.class.php:50
 msgid "Single Sign-on Provider"
-msgstr "Provedor Single Sign-on"
+msgstr "Provedor de Logon Único"
+
+#: inc/preference.class.php:172
+msgid "Linked accounts"
+msgstr "Contas vinculadas"
 
 #: inc/provider.class.php:98
 msgid "SSO Type"
 msgstr "Tipo SSO"
 
-#: inc/provider.class.php:106 inc/provider.class.php:298
+#: inc/provider.class.php:106 inc/provider.class.php:426
 msgid "Client ID"
 msgstr "ID de Cliente"
 
-#: inc/provider.class.php:108 inc/provider.class.php:306
+#: inc/provider.class.php:108 inc/provider.class.php:434
 msgid "Client Secret"
 msgstr "Segredo do Cliente"
 
-#: inc/provider.class.php:113 inc/provider.class.php:314
+#: inc/provider.class.php:113 inc/provider.class.php:442
 msgid "Scope"
 msgstr "Escopo"
 
-#: inc/provider.class.php:115 inc/provider.class.php:322
+#: inc/provider.class.php:115 inc/provider.class.php:450
 msgid "Extra Options"
 msgstr "Opções Extras"
 
-#: inc/provider.class.php:126 inc/provider.class.php:330
+#: inc/provider.class.php:126 inc/provider.class.php:458
 msgid "Authorize URL"
 msgstr "URL de Autorização"
 
-#: inc/provider.class.php:131 inc/provider.class.php:338
+#: inc/provider.class.php:131 inc/provider.class.php:466
 msgid "Access Token URL"
 msgstr "URL de Token de Acesso"
 
-#: inc/provider.class.php:136 inc/provider.class.php:346
+#: inc/provider.class.php:136 inc/provider.class.php:474
 msgid "Resource Owner Details URL"
 msgstr "URL de Detalhes do Proprietário do Recurso"
 
-#: inc/provider.class.php:144
+#: inc/provider.class.php:224
 msgid "Callback URL"
-msgstr ""
+msgstr "URL de Retorno"
 
-#: inc/provider.class.php:148
-#, fuzzy
+#: inc/provider.class.php:228
 msgid "Test Single Sign-on"
-msgstr "Single Sign-on"
+msgstr "Testar Logon Único"
 
-#: inc/provider.class.php:197
+#: inc/provider.class.php:286
 msgid "A Name is required"
 msgstr "Nome é obrigatório"
 
-#: inc/provider.class.php:203
+#: inc/provider.class.php:292
 #, php-format
 msgid "The \"%s\" is a Invalid type"
 msgstr "O \"%s\" é um tipo inválido"
 
-#: inc/provider.class.php:207
+#: inc/provider.class.php:296
 msgid "A Client ID is required"
 msgstr "ID de cliente é obrigatório"
 
-#: inc/provider.class.php:211
+#: inc/provider.class.php:300
 msgid "A Client Secret is required"
 msgstr "Segredo do Cliente é obrigatório"
 
-#: inc/provider.class.php:216
+#: inc/provider.class.php:305
 msgid "An Authorize URL is required"
 msgstr "URL de Autorização é obrigatório"
 
-#: inc/provider.class.php:218
+#: inc/provider.class.php:307
 msgid "The Authorize URL is invalid"
 msgstr "A URL de Autorização é inválida"
 
-#: inc/provider.class.php:222
+#: inc/provider.class.php:311
 msgid "An Access Token URL is required"
 msgstr "URL de Token de Acesso é obrigatório"
 
-#: inc/provider.class.php:224
+#: inc/provider.class.php:313
 msgid "The Access Token URL is invalid"
 msgstr "A URL de Token de Acesso é inválida"
 
-#: inc/provider.class.php:228
+#: inc/provider.class.php:317
 msgid "A Resource Owner Details URL is required"
 msgstr "URL de Detalhes do Proprietário do Recurso é obrigatório"
 
-#: inc/provider.class.php:230
+#: inc/provider.class.php:319
 msgid "The Resource Owner Details URL is invalid"
 msgstr "A URL de Detalhes do Proprietário do Recurso é inválida"
 
-#: inc/provider.class.php:405
+#: inc/provider.class.php:533
 msgid "Generic"
-msgstr "Generic"
+msgstr "Genérico"
 
-#: inc/provider.class.php:406
+#: inc/provider.class.php:534
 msgid "Facebook"
 msgstr "Facebook"
 
-#: inc/provider.class.php:407
+#: inc/provider.class.php:535
 msgid "GitHub"
 msgstr "GitHub"
 
-#: inc/provider.class.php:408
+#: inc/provider.class.php:536
 msgid "Google"
 msgstr "Google"
 
-#: inc/provider.class.php:409
+#: inc/provider.class.php:537
 msgid "Instagram"
 msgstr "Instagram"
 
-#: inc/provider.class.php:410
+#: inc/provider.class.php:538
 msgid "LinkdeIn"
 msgstr "LinkdeIn"
 
+#: inc/toolbox.class.php:154
+#, php-format
+msgid "Login with %s"
+msgstr "Entrar com %s"
+
 #~ msgid "Run first: composer install"
 #~ msgstr "Execute primeiro: composer install"

+ 41 - 36
locales/singlesignon.pot

@@ -6,10 +6,10 @@
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: singlesignon 1.0.0\n"
+"Project-Id-Version: singlesignon 1.3.0\n"
 "Report-Msgid-Bugs-To: https://github.com/edgardmessias/glpi-singlesignon/"
 "issues\n"
-"POT-Creation-Date: 2021-01-12 13:25-0300\n"
+"POT-Creation-Date: 2021-01-20 14:48-0300\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,22 +18,18 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: hook.php:27 hook.php:75
-#, php-format
-msgid "Login with %s"
-msgstr ""
-
 #: setup.php:8
 #, php-format
 msgid "Please, rename the plugin folder \"%s\" to \"singlesignon\""
 msgstr ""
 
-#: setup.php:36 front/provider.form.php:59 front/provider.form.php:61
-#: front/provider.php:6 front/provider.php:8 inc/provider.class.php:57
+#: setup.php:40 front/provider.form.php:59 front/provider.form.php:61
+#: front/provider.php:6 front/provider.php:8 inc/preference.class.php:64
+#: inc/provider.class.php:57
 msgid "Single Sign-on"
 msgstr ""
 
-#: setup.php:53
+#: setup.php:57
 msgid "This plugin requires GLPI >= 0.85"
 msgstr ""
 
@@ -49,111 +45,120 @@ msgstr ""
 msgid "Provider not active."
 msgstr ""
 
-#: inc/provider.class.php:50
+#: inc/preference.class.php:147 inc/provider.class.php:50
 msgid "Single Sign-on Provider"
 msgstr ""
 
+#: inc/preference.class.php:172
+msgid "Linked accounts"
+msgstr ""
+
 #: inc/provider.class.php:98
 msgid "SSO Type"
 msgstr ""
 
-#: inc/provider.class.php:106 inc/provider.class.php:298
+#: inc/provider.class.php:106 inc/provider.class.php:426
 msgid "Client ID"
 msgstr ""
 
-#: inc/provider.class.php:108 inc/provider.class.php:306
+#: inc/provider.class.php:108 inc/provider.class.php:434
 msgid "Client Secret"
 msgstr ""
 
-#: inc/provider.class.php:113 inc/provider.class.php:314
+#: inc/provider.class.php:113 inc/provider.class.php:442
 msgid "Scope"
 msgstr ""
 
-#: inc/provider.class.php:115 inc/provider.class.php:322
+#: inc/provider.class.php:115 inc/provider.class.php:450
 msgid "Extra Options"
 msgstr ""
 
-#: inc/provider.class.php:126 inc/provider.class.php:330
+#: inc/provider.class.php:126 inc/provider.class.php:458
 msgid "Authorize URL"
 msgstr ""
 
-#: inc/provider.class.php:131 inc/provider.class.php:338
+#: inc/provider.class.php:131 inc/provider.class.php:466
 msgid "Access Token URL"
 msgstr ""
 
-#: inc/provider.class.php:136 inc/provider.class.php:346
+#: inc/provider.class.php:136 inc/provider.class.php:474
 msgid "Resource Owner Details URL"
 msgstr ""
 
-#: inc/provider.class.php:144
+#: inc/provider.class.php:224
 msgid "Callback URL"
 msgstr ""
 
-#: inc/provider.class.php:148
+#: inc/provider.class.php:228
 msgid "Test Single Sign-on"
 msgstr ""
 
-#: inc/provider.class.php:197
+#: inc/provider.class.php:286
 msgid "A Name is required"
 msgstr ""
 
-#: inc/provider.class.php:203
+#: inc/provider.class.php:292
 #, php-format
 msgid "The \"%s\" is a Invalid type"
 msgstr ""
 
-#: inc/provider.class.php:207
+#: inc/provider.class.php:296
 msgid "A Client ID is required"
 msgstr ""
 
-#: inc/provider.class.php:211
+#: inc/provider.class.php:300
 msgid "A Client Secret is required"
 msgstr ""
 
-#: inc/provider.class.php:216
+#: inc/provider.class.php:305
 msgid "An Authorize URL is required"
 msgstr ""
 
-#: inc/provider.class.php:218
+#: inc/provider.class.php:307
 msgid "The Authorize URL is invalid"
 msgstr ""
 
-#: inc/provider.class.php:222
+#: inc/provider.class.php:311
 msgid "An Access Token URL is required"
 msgstr ""
 
-#: inc/provider.class.php:224
+#: inc/provider.class.php:313
 msgid "The Access Token URL is invalid"
 msgstr ""
 
-#: inc/provider.class.php:228
+#: inc/provider.class.php:317
 msgid "A Resource Owner Details URL is required"
 msgstr ""
 
-#: inc/provider.class.php:230
+#: inc/provider.class.php:319
 msgid "The Resource Owner Details URL is invalid"
 msgstr ""
 
-#: inc/provider.class.php:405
+#: inc/provider.class.php:533
 msgid "Generic"
 msgstr ""
 
-#: inc/provider.class.php:406
+#: inc/provider.class.php:534
 msgid "Facebook"
 msgstr ""
 
-#: inc/provider.class.php:407
+#: inc/provider.class.php:535
 msgid "GitHub"
 msgstr ""
 
-#: inc/provider.class.php:408
+#: inc/provider.class.php:536
 msgid "Google"
 msgstr ""
 
-#: inc/provider.class.php:409
+#: inc/provider.class.php:537
 msgid "Instagram"
 msgstr ""
 
-#: inc/provider.class.php:410
+#: inc/provider.class.php:538
 msgid "LinkdeIn"
 msgstr ""
+
+#: inc/toolbox.class.php:154
+#, php-format
+msgid "Login with %s"
+msgstr ""

+ 5 - 1
setup.php

@@ -1,6 +1,6 @@
 <?php
 
-define('PLUGIN_SINGLESIGNON_VERSION', '1.2.0');
+define('PLUGIN_SINGLESIGNON_VERSION', '1.3.0');
 
 $folder = basename(dirname(__FILE__));
 
@@ -19,6 +19,10 @@ function plugin_init_singlesignon() {
       include_once $autoload;
    }
 
+   Plugin::registerClass('PluginSinglesignonPreference', [
+      'addtabon' => ['Preference', 'User']
+   ]);
+
    $PLUGIN_HOOKS['csrf_compliant']['singlesignon'] = true;
 
    $CFG_SSO = Config::getConfigurationValues('singlesignon');