소스 검색

Merge branch 'master' into patch-1

Eduardo Mozart de Oliveira 11 달 전
부모
커밋
9e81712eb7

+ 83 - 0
.github/ISSUE_TEMPLATE/bug_report.yml

@@ -0,0 +1,83 @@
+name: Bug Report
+description: Create a report to help us improve glpi-singlesignon
+body:
+  - type: markdown
+    attributes:
+      value: |
+
+        Dear GLPI plugin user.
+
+        BEFORE SUBMITTING YOUR ISSUE, please make sure to read and follow these steps:
+
+        * We do not guarantee any processing / resolution time for community issues.
+        * Keep this tracker in ENGLISH. If you want support in your language, the [community forum](https://forum.glpi-project.org) is the best place.
+        * Always try to reproduce your issue at least on latest stable release.
+
+        The GLPI Single-Sign On Development team.
+  - type: checkboxes
+    attributes:
+      label: Is there an existing issue for this?
+      description: Please search to see if an issue already exists for the bug you encountered.
+      options:
+        - label: I have searched the existing issues
+    validations:
+      required: true
+  - type: input
+    id: glpi-version
+    attributes:
+      label: GLPI Version
+      description: What version of our GLPI are you running?
+    validations:
+      required: true
+  - type: input
+    id: plugin-version
+    attributes:
+      label: Plugin version
+      description: What version of `glpi-singlesignon` are you running?
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Bug description
+      description: A concise description of the problem you are experiencing and what you expected to happen.
+    validations:
+      required: false
+  - type: textarea
+    id: logs
+    attributes:
+      label: Relevant log output
+      description: |
+        Please copy and paste any relevant log output. Find them in `*-error.log` files under `glpi/files/_log/`.
+
+        Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
+      render: shell
+  - type: input
+    id: url
+    attributes:
+      label: Page URL
+      description: If applicable, page URL where the bug happens.
+    validations:
+      required: false
+  - type: textarea
+    attributes:
+      label: Steps To reproduce
+      description: Steps to reproduce the behavior.
+      placeholder: |
+        1. With this config...
+        2. Go to...
+        3. Scroll down to...
+        4. See error...
+    validations:
+      required: false
+  - type: textarea
+    attributes:
+      label: Your GLPI setup information
+      description: Please copy and paste information you will find in GLPI in `Setup > General` menu, `System` tab.
+    validations:
+      required: false
+  - type: textarea
+    attributes:
+      label: Anything else?
+      description: Add any other context about the problem here.
+    validations:
+      required: false

+ 89 - 0
.github/workflows/release.yml

@@ -0,0 +1,89 @@
+name: Create release with tag
+
+on:
+   push:
+      # Sequence of patterns matched against refs/heads
+      branches:
+      - '*.*.*' # Push events to matching ex:20.15.10
+
+env:
+    TAG_VALUE: ${GITHUB_REF/refs\/heads\//}
+    PR_BRANCH: release-ci-${{ github.sha }}
+
+jobs:
+    generate-changelog:
+      name: Generate Changelog
+      runs-on: ubuntu-latest
+      steps:
+          - name: Checkout code
+            uses: actions/checkout@v2
+
+          - name: 🏷️ Create/update tag
+            uses: actions/github-script@v7
+            with:
+              script: |
+                github.rest.git.createRef({
+                  owner: context.repo.owner,
+                  repo: context.repo.repo,
+                  ref: 'refs/tags/${{ github.ref_name }}',
+                  sha: context.sha
+                }).catch(err => {
+                  if (err.status !== 422) throw err;
+                })
+
+          - name: Update CHANGELOG
+            id: changelog
+            uses: requarks/changelog-action@v1
+            with:
+              token: ${{ github.token }}
+              tag: ${{ github.ref_name }}
+              writeToFile: true
+              #excludeTypes: build,docs,style
+              #includeInvalidCommits: true
+
+          - name: Commit CHANGELOG.md
+            uses: stefanzweifel/git-auto-commit-action@v4
+            with:
+              branch: master
+              commit_message: 'docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]'
+              file_pattern: CHANGELOG.md
+
+    build:
+      name: Upload Release Asset
+      permissions: write-all
+      runs-on: ubuntu-latest
+      steps:
+        - name: Checkout code
+          uses: actions/checkout@v2
+          
+        - name: Build project # This would actually build your project, using zip for an example artifact
+          id: build_
+          env:
+              GITHUB_NAME: ${{ github.event.repository.name }}
+          run: sudo apt-get install libxml-xpath-perl;echo $(xpath -e '/root/versions/version[num="'${GITHUB_REF/refs\/heads\/v/}'"]/compatibility/text()' plugin.xml);echo ::set-output name=version_glpi::$(xpath -e '/root/versions/version[num="'${GITHUB_REF/refs\/heads\/v/}'"]/compatibility/text()' plugin.xml); rm -rf plugin.xml tools wiki screenshots test .git .github ISSUE_TEMPLATE.md TODO.txt $GITHUB_NAME.png;cd ..; tar jcvf $GITHUB_NAME-${GITHUB_REF/refs\/heads\//}.tar.bz2 $GITHUB_NAME;ls -al;echo ::set-output name=tag::${GITHUB_REF/refs\/heads\//};echo ${{ steps.getxml.outputs.info }};
+         # run: rm -rf plugin.xml tools wiki screenshots test ISSUE_TEMPLATE.md TODO.txt $GITHUB_NAME.png; tar -zcvf $GITHUB_NAME-$GITHUB_TAG.tar.gz $GITHUB_NAME
+
+        - name: Create Release
+          id: create_release
+          uses: actions/create-release@v1
+          env:
+            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          with:
+            tag_name: ${{ steps.build_.outputs.tag }}
+            release_name: | 
+                      GLPI ${{ steps.build_.outputs.version_glpi }} : Version ${{ steps.build_.outputs.tag }} disponible / available
+            body : Version ${{ steps.build_.outputs.tag }} released for GLPI ${{ steps.build_.outputs.version_glpi }}. The [full changelog is available](https://github.com/edgardmessias/glpi-singlesignon/blob/${{ steps.build_.outputs.tag }}/CHANGELOG.md) for more details.
+            draft: false
+            prerelease: false
+
+        - name: Upload Release Asset
+          id: upload-release-asset 
+          uses: actions/upload-release-asset@v1
+          env:
+            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+            GITHUB_NAME: ${{ github.event.repository.name }}
+          with:
+            upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 
+            asset_path: /home/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}-${{ steps.build_.outputs.tag }}.tar.bz2
+            asset_name: ${{ github.event.repository.name }}-${{ steps.build_.outputs.tag }}.tar.bz2
+            asset_content_type: application/zip

+ 1 - 1
.gitignore

@@ -1,7 +1,7 @@
-/locales/*.mo
 /nbproject
 /vendor
 /*.tar
 /*.tar.gz
 /*.tgz
 /*.zip
+.DS_Store

+ 5 - 3
README.md

@@ -24,7 +24,7 @@ Single sign-on (SSO) is a property of access control of multiple related, yet in
  * Instagram - https://www.instagram.com/developer/authentication/
  * LinkedIn - https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin/context
  * Generic - Allow to define custom URLs
- * Zitadel - use _Generic_ and see parameters in [Generic Examples - Zitadel](generic_examples/zitadel.md)
+ * Zitadel - use _Generic_ and see parameters in [Generic Examples - Zitadel](https://github.com/edgardmessias/glpi-singlesignon/wiki/Generic-Examples-%E2%80%90-Zitadel)
 
 # Adding translations
 If your preferred language is missing. You can add your own translation with the following steps:
@@ -38,6 +38,9 @@ If your preferred language is missing. You can add your own translation with the
  * If msgfmt is not found, install the package gettext (apt install -y gettext)
  * If you edit a previous translation, you may need to update the translation cache: go to Setup - General - Performance, enable Debug mode, clear translation cache
 
+# Adding a new release
+To create a new release of this plugin automatically through GitHub Actions (Workflow), edit the file ``plugin.xml`` to include the new version tag (e.g. ``v1.3.4``), GLPI compatible version and download URL and create a new branch.
+
 # Screenshots
 
 ![image 1](./screenshots/image_1.png)
@@ -48,14 +51,13 @@ If your preferred language is missing. You can add your own translation with the
  <tr>
     <td align="center">
     PayPal <br>
-       <img src="https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=https://www.paypal.com/donate?hosted_button_id=5KHYY5ZDTNDSY"> <br>
        <a href="https://www.paypal.com/donate?hosted_button_id=5KHYY5ZDTNDSY">
           <img src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif">
        </a>
     </td>
     <td align="center">
        Pix (Brazil) <br>
-       <img src="https://chart.googleapis.com/chart?chs=250x250&cht=qr&chl=00020126680014BR.GOV.BCB.PIX013628571c52-8b9b-416c-a18f-8e52460608810206Doa%C3%A7%C3%A3o5204000053039865802BR5923Edgard%20Lorraine%20Messias6009SAO%20PAULO61080540900062160512NU50UnEaVM0H63042A45"> <br>
+       <img src="https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=00020126680014BR.GOV.BCB.PIX013628571c52-8b9b-416c-a18f-8e52460608810206Doa%C3%A7%C3%A3o5204000053039865802BR5923Edgard%20Lorraine%20Messias6009SAO%20PAULO61080540900062160512NU50UnEaVM0H63042A45"> <br>
        28571c52-8b9b-416c-a18f-8e5246060881
     </td>
  </tr>

+ 10 - 17
front/callback.php

@@ -56,7 +56,7 @@ $test = PluginSinglesignonToolbox::getCallbackParameters('test');
 
 if ($test) {
    $signon_provider->debug = true;
-   Html::nullHeader("Login", $CFG_GLPI["root_doc"] . '/index.php');
+   Html::nullHeader("Login", PluginSinglesignonToolbox::getBaseURL() . '/index.php');
    echo '<div class="left spaced">';
    echo '<pre>';
    echo "### BEGIN ###\n";
@@ -83,34 +83,27 @@ if ($user_id || $signon_provider->login()) {
 
    if (isset($params['redirect'])) {
       $REDIRECT = '?redirect=' . $params['redirect'];
-   } else if (isset($_GET['state']) && is_integer(strpos($_GET['state'], "&redirect="))) {
-      $REDIRECT = '?' . substr($_GET['state'], strpos($_GET['state'], "&redirect=") + 1);
+   } else if (isset($_GET['state']) && is_integer(strpos($_GET['state'], ";redirect="))) {
+      $REDIRECT = '?' . substr($_GET['state'], strpos($_GET['state'], ";redirect=") + 1);
    }
 
    $url_redirect = '';
 
-
-   if (isset($params['redirect'])) {
-      $REDIRECT = '?redirect=' . $params['redirect'];
-   } else if (isset($_GET['state']) && is_integer(strpos($_GET['state'], "&redirect="))) {
-      $REDIRECT = '?' . substr($_GET['state'], strpos($_GET['state'], "&redirect=") + 1);
-   }
-
    if ($_SESSION["glpiactiveprofile"]["interface"] == "helpdesk") {
       if ($_SESSION['glpiactiveprofile']['create_ticket_on_login'] && empty($REDIRECT)) {
-         $url_redirect = $CFG_GLPI['root_doc'] . "/front/helpdesk.public.php?create_ticket=1";
+         $url_redirect = PluginSinglesignonToolbox::getBaseURL() . "/front/helpdesk.public.php?create_ticket=1";
       } else {
-         $url_redirect = $CFG_GLPI['root_doc'] . "/front/helpdesk.public.php$REDIRECT";
+         $url_redirect = PluginSinglesignonToolbox::getBaseURL() . "/front/helpdesk.public.php$REDIRECT";
       }
    } else {
       if ($_SESSION['glpiactiveprofile']['create_ticket_on_login'] && empty($REDIRECT)) {
-         $url_redirect = $CFG_GLPI['root_doc'] . "/front/ticket.form.php";
+         $url_redirect = PluginSinglesignonToolbox::getBaseURL() . "/front/ticket.form.php";
       } else {
-         $url_redirect = $CFG_GLPI['root_doc'] . "/front/central.php$REDIRECT";
+         $url_redirect = PluginSinglesignonToolbox::getBaseURL() . "/front/central.php$REDIRECT";
       }
    }
 
-   Html::nullHeader("Login", $CFG_GLPI["root_doc"] . '/index.php');
+   Html::nullHeader("Login", PluginSinglesignonToolbox::getBaseURL() . '/index.php');
    echo '<div class="center spaced"><a href="' . $url_redirect . '">' .
    __sso('Automatic redirection, else click') . '</a>';
    echo '<script type="text/javascript">
@@ -129,10 +122,10 @@ if ($user_id || $signon_provider->login()) {
 }
 
 // we have done at least a good login? No, we exit.
-Html::nullHeader("Login", $CFG_GLPI["root_doc"] . '/index.php');
+Html::nullHeader("Login", PluginSinglesignonToolbox::getBaseURL() . '/index.php');
 echo '<div class="center b">' . __('User not authorized to connect in GLPI') . '<br><br>';
 // Logout whit noAUto to manage auto_login with errors
-echo '<a href="' . $CFG_GLPI["root_doc"] . '/front/logout.php?noAUTO=1' .
+echo '<a href="' . PluginSinglesignonToolbox::getBaseURL() . '/front/logout.php?noAUTO=1' .
 str_replace("?", "&", $REDIRECT) . '" class="singlesignon">' . __('Log in again') . '</a></div>';
 echo '<script type="text/javascript">
    if (window.opener) {

+ 0 - 29
generic_examples/zitadel.md

@@ -1,29 +0,0 @@
-- Go to Zitadel and login as admin
-- Create a new Project, name for example: glpi
-- Create a new Application in this new project, name for example: glpi
-  - Type of application: WEB
-  - Authentication Method: CODE
-  - Redirect URIs: empty, will be set later
-  - Create
-  - Save Client-ID and Client-Secret, we need it later
-- Go to GLPI and login as a Super-Admin
-- Install and activate the plugin
-- Go to the plugin settings
-- Create your first Single Sign-on Provider with the following options
-  - Name: choose one you like
-  - Client ID: the previous saved Client-ID from Zitadel
-  - client Secret: the previous saved Client-Secret from Zitadel
-  - Scope: openid email profile
-  - Authorize URL: https://zitadel.example.com/oauth/v2/authorize
-  - Access Token URL: https://zitadel.example.com/oauth/v2/token
-  - Resource Owner Details URL: https://zitadel.example.com/oidc/v1/userinfo
-  - SplitDomain: Yes
-  - Split Name: Yes
-  - Save Settings
-- Copy the Callback URL and go back to Zitadel
-- Open the project and the application, then go to Redirect Settings
-- Paste the URL in Redirect URIs, click on the plus and save the settings
-- Go back to GLPI and open the provider in the plugins settings again, now press Test Single Sign-on
-- A Pop-Up should open and close after a few seconds and the side reloads
-- Your current account should be linked now to your current Zitadel Account
-- All users from Zitadel can login into glpi and will be created there automatically

+ 1 - 1
inc/preference.class.php

@@ -116,7 +116,7 @@ class PluginSinglesignonPreference extends CommonDBTM {
       }
       $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 "<form name='form' action=\"" . PluginSinglesignonToolbox::getBaseURL() . Plugin::getPhpDir("singlesignon", false) . "/front/user.form.php\" method='post'>";
       }
       echo Html::hidden('user_id', ['value' => $this->user_id]);
 

+ 160 - 72
inc/provider.class.php

@@ -179,7 +179,7 @@ class PluginSinglesignonProvider extends CommonDBTM {
       echo "</td>";
       echo "<td>" . __sso('AuthorizedDomains');
       echo "&nbsp;";
-      Html::showToolTip(nl2br(__sso('AuthorizedDomainsTooltip')));
+      Html::showToolTip(nl2br(__sso('Provide a list of domains allowed to log in through this provider (separated by commas, no spaces).')));
       echo "</td>";
       echo "<td><input type='text' style='width:96%' name='authorized_domains' value='" . $this->fields["authorized_domains"] . "'></td>";
       echo "</td></tr>\n";
@@ -274,7 +274,7 @@ class PluginSinglesignonProvider extends CommonDBTM {
          echo "</tr>\n";
 
          $url = PluginSinglesignonToolbox::getCallbackUrl($ID);
-         $fullUrl = $this->getBaseURL() . $url;
+         $fullUrl = PluginSinglesignonToolbox::getBaseURL() . $url;
          echo "<tr class='tab_bg_1'>";
          echo "<td>" . __sso('Callback URL') . "</td>";
          echo "<td colspan='3'><a id='singlesignon_callbackurl' href='$fullUrl' data-url='$url'>$fullUrl</a></td>";
@@ -660,7 +660,8 @@ class PluginSinglesignonProvider extends CommonDBTM {
     *
     * @return string
     * */
-   static function getHistoryEntry($data) {
+   // phpcs:disable
+   /* static function getHistoryEntry($data) {
 
       switch ($data['linked_action'] - Log::HISTORY_PLUGIN) {
          case 0:
@@ -668,7 +669,8 @@ class PluginSinglesignonProvider extends CommonDBTM {
       }
 
       return '';
-   }
+   } */
+   // phpcs:enable
 
    //////////////////////////////
    ////// SPECIFIC MODIF MASSIVE FUNCTIONS ///////
@@ -678,7 +680,8 @@ class PluginSinglesignonProvider extends CommonDBTM {
     *
     * @see CommonDBTM::getSpecificMassiveActions()
     * */
-   function getSpecificMassiveActions($checkitem = null) {
+   // phpcs:disable
+   /* function getSpecificMassiveActions($checkitem = null) {
 
       $actions = parent::getSpecificMassiveActions($checkitem);
 
@@ -686,14 +689,16 @@ class PluginSinglesignonProvider extends CommonDBTM {
       $actions[__CLASS__ . MassiveAction::CLASS_ACTION_SEPARATOR . 'do_nothing'] = __('Do Nothing - just for fun', 'example');  // Specific one
 
       return $actions;
-   }
+   } */
+   // phpcs:enable
 
    /**
     * @since version 0.85
     *
     * @see CommonDBTM::showMassiveActionsSubForm()
     * */
-   static function showMassiveActionsSubForm(MassiveAction $ma) {
+   // phpcs:disable
+   /* static function showMassiveActionsSubForm(MassiveAction $ma) {
 
       switch ($ma->getAction()) {
          case 'DoIt':
@@ -704,14 +709,16 @@ class PluginSinglesignonProvider extends CommonDBTM {
             return true;
       }
       return parent::showMassiveActionsSubForm($ma);
-   }
+   } */
+   // phpcs:enable
 
    /**
     * @since version 0.85
     *
     * @see CommonDBTM::processMassiveActionsForOneItemtype()
     * */
-   static function processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBTM $item, array $ids) {
+   // phpcs:disable
+   /* static function processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBTM $item, array $ids) {
       global $DB;
 
       switch ($ma->getAction()) {
@@ -755,7 +762,8 @@ class PluginSinglesignonProvider extends CommonDBTM {
             return;
       }
       parent::processMassiveActionsForOneItemtype($ma, $item, $ids);
-   }
+   } */
+   // phpcs:enable
 
    static function getIcon() {
       return "fas fa-user-lock";
@@ -876,56 +884,6 @@ class PluginSinglesignonProvider extends CommonDBTM {
       return $url;
    }
 
-   /**
-    * Get current URL without query string
-    * @return string
-    */
-   private function getBaseURL() {
-      $baseURL = "";
-      if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
-         $baseURL = ($_SERVER["HTTP_X_FORWARDED_PROTO"] == "https") ? "https://" : "http://";
-      } else if (isset($_SERVER["HTTPS"])) {
-         $baseURL = ($_SERVER["HTTPS"] == "on") ? "https://" : "http://";
-      } else {
-         $baseURL = "http://";
-      }
-      if (isset($_SERVER["HTTP_X_FORWARDED_HOST"])) {
-         $baseURL .= $_SERVER["HTTP_X_FORWARDED_HOST"];
-      } else if (isset($_SERVER["HTTP_X_FORWARDED_HOST"])) {
-         $baseURL .= $_SERVER["HTTP_X_FORWARDED_HOST"];
-      } else {
-         $baseURL .= $_SERVER["SERVER_NAME"];
-      }
-
-      $port = $_SERVER["SERVER_PORT"];
-      if (isset($_SERVER["HTTP_X_FORWARDED_PORT"])) {
-         $port = $_SERVER["HTTP_X_FORWARDED_PORT"];
-      }
-
-      if ($port != "80" && $port != "443") {
-         $baseURL .= ":" . $_SERVER["SERVER_PORT"];
-      }
-      return $baseURL;
-   }
-
-   /**
-    * Get current URL without query string
-    * @return string
-    */
-   private function getCurrentURL() {
-      $currentURL = $this->getBaseURL();
-
-      // $currentURL .= $_SERVER["REQUEST_URI"];
-      // Ignore Query String
-      if (isset($_SERVER["SCRIPT_NAME"])) {
-         $currentURL .= $_SERVER["SCRIPT_NAME"];
-      }
-      if (isset($_SERVER["PATH_INFO"])) {
-         $currentURL .= $_SERVER["PATH_INFO"];
-      }
-      return $currentURL;
-   }
-
    /**
     *
     * @return boolean|string
@@ -950,7 +908,7 @@ class PluginSinglesignonProvider extends CommonDBTM {
             'state' => $state,
             'response_type' => 'code',
             'approval_prompt' => 'auto',
-            'redirect_uri' => $this->getCurrentURL(),
+            'redirect_uri' => PluginSinglesignonToolbox::getCurrentURL(),
          ];
 
          $params = Plugin::doHookFunction("sso:authorize_params", $params);
@@ -964,8 +922,8 @@ class PluginSinglesignonProvider extends CommonDBTM {
          exit;
       }
 
-      if (isset($_GET['state']) && is_integer(strpos($_GET['state'], "&redirect="))) {
-         $pos_redirect  = strpos($_GET['state'], "&redirect=");
+      if (isset($_GET['state']) && is_integer(strpos($_GET['state'], ";redirect="))) {
+         $pos_redirect  = strpos($_GET['state'], ";redirect=");
          $state         = substr($_GET['state'], 0, $pos_redirect);
          $_GET['state'] = substr($_GET['state'], $pos_redirect);
       } else {
@@ -997,7 +955,7 @@ class PluginSinglesignonProvider extends CommonDBTM {
       $params = [
          'client_id' => $this->getClientId(),
          'client_secret' => $this->getClientSecret(),
-         'redirect_uri' => $this->getCurrentURL(),
+         'redirect_uri' => PluginSinglesignonToolbox::getCurrentURL(),
          'grant_type' => 'authorization_code',
          'code' => $this->_code,
       ];
@@ -1025,6 +983,14 @@ class PluginSinglesignonProvider extends CommonDBTM {
          if ($this->debug) {
             print_r($data);
          }
+         if (isset($data['error_description'])) {
+            echo '<style>#page .center small { font-weight: normal; }</style>
+            <script type="text/javascript">
+            window.onload = function() {
+               $("#page .center").append("<br><br><small>' . $data['error_description'] . '</small>");
+            };
+            </script>';
+         }
          if (!isset($data['access_token'])) {
             return false;
          }
@@ -1256,11 +1222,13 @@ class PluginSinglesignonProvider extends CommonDBTM {
          $userPost = [
             'name' => $login,
             'add' => 1,
+            'password' => '',
             'realname' => $realname,
             'firstname' => $firstname,
             //'picture' => $resource_array['picture'] ?? '',
             'picture' => $resource_array['picture'],
             'api_token' => $tokenAPI,
+            'api_token_date' => date("Y-m-d H:i:s"),
             'personal_token' => $tokenPersonnel,
             'is_active' => 1
          ];
@@ -1282,9 +1250,11 @@ class PluginSinglesignonProvider extends CommonDBTM {
             $userPost = [
                'name' => $login,
                'add' => 1,
+               'password' => '',
                'realname' => $firstLastArray[1],
                'firstname' => $firstLastArray[0],
                'api_token' => $tokenAPI,
+               'api_token_date' => date("Y-m-d H:i:s"),
                'personal_token' => $tokenPersonnel,
                'is_active' => 1
             ];
@@ -1358,23 +1328,38 @@ class PluginSinglesignonProvider extends CommonDBTM {
       if (!$user) {
          return false;
       }
+
+      $this->syncOAuthPhoto($user);
+     
+      // Create fake auth
+      /* $auth = new Auth();
+      $auth->user = $user;
+      $auth->auth_succeded = true;
+      $auth->extauth = 1;
+      $auth->user_present = 1;
+      $auth->user->fields['authtype'] = Auth::DB_GLPI;
+
+      Session::init($auth);
+
+      // Return false if the profile is not defined in Session::init($auth)
+      return $auth->auth_succeded; */
 	  
-	  global $DB;
+	    global $DB;
 	  
-	  $userId = $user->fields['id'];
+	    $userId = $user->fields['id'];
 
       // Set a random password for the current user
       $tempPassword = bin2hex(random_bytes(64));
-	  $DB->update('glpi_users', ['password' => Auth::getPasswordHash($tempPassword)], ['id' => $userId]);
+	    $DB->update('glpi_users', ['password' => Auth::getPasswordHash($tempPassword)], ['id' => $userId]);
 
       // Log-in using the generated password as if you were logging in using the login form
-	  $auth = new Auth();
-	  $authResult = $auth->login($user->fields['name'], $tempPassword);
+	    $auth = new Auth();
+	    $authResult = $auth->login($user->fields['name'], $tempPassword);
 	  
-	  // Rollback password change
-	  $DB->update('glpi_users', ['password' => $user->fields['password']], ['id' => $userId]);
+	    // Rollback password change
+	    $DB->update('glpi_users', ['password' => $user->fields['password']], ['id' => $userId]);
 	   
-	  return $authResult;
+	    return $authResult;
    }
 
    public function linkUser($user_id) {
@@ -1418,4 +1403,107 @@ class PluginSinglesignonProvider extends CommonDBTM {
          'remote_id' => $remote_id,
       ]);
    }
+
+
+   /**
+    * Synchronize picture (photo) of the user.
+    *
+    * @return string|boolean Filename to be stored in user picture field, false if no picture found
+    */
+   public function syncOAuthPhoto($user) {
+      $token = $this->getAccessToken();
+      if (!$token) {
+         return false;
+      }
+
+      $url = $this->getResourceOwnerDetailsUrl($token);
+
+      $headers = [
+         "Authorization:Bearer $token"
+      ];
+
+      $headers = Plugin::doHookFunction("sso:resource_owner_picture", $headers);
+
+      if ($this->debug) {
+         print_r("\nsyncOAuthPhoto:\n");
+      }
+
+      //get picture content (base64) in Azure
+      if (preg_match("/^(?:https?:\/\/)?(?:[^.]+\.)?graph\.microsoft\.com(\/.*)?$/", $url)) {
+         array_push($headers, "Content-Type:image/jpeg; charset=utf-8");
+
+         $photo_url = "https://graph.microsoft.com/v1.0/me/photo/\$value";
+         $img = Toolbox::callCurl($photo_url, [
+            CURLOPT_HTTPHEADER => $headers,
+            CURLOPT_SSL_VERIFYHOST => false,
+            CURLOPT_SSL_VERIFYPEER => false,
+         ]);
+         if (!empty($img)) {
+            /* if ($this->debug) {
+            print_r($content);
+            } */
+
+            //prepare paths
+            $filename  = uniqid($user->fields['id'] . '_');
+            $sub       = substr($filename, -2); /* 2 hex digit */
+            $file      = GLPI_PICTURE_DIR . "/{$sub}/{$filename}.jpg";
+
+            if (array_key_exists('picture', $user->fields)) {
+               $oldfile = GLPI_PICTURE_DIR . "/" . $user->fields["picture"];
+            } else {
+               $oldfile = null;
+            }
+
+            //update picture if not exist or changed
+            if (empty($user->fields["picture"])
+               || !file_exists($oldfile)
+               || sha1_file($oldfile) !== sha1($img)
+            ) {
+
+               if (!is_dir(GLPI_PICTURE_DIR . "/$sub")) {
+                  mkdir(GLPI_PICTURE_DIR . "/$sub");
+               }
+
+               //save picture
+               $outjpeg = fopen($file, 'wb');
+               fwrite($outjpeg, $img);
+               fclose($outjpeg);
+
+               //save thumbnail
+               $thumb = GLPI_PICTURE_DIR . "/{$sub}/{$filename}_min.jpg";
+               Toolbox::resizePicture($file, $thumb);
+
+               $user->fields['picture'] = "{$sub}/{$filename}.jpg";
+               $success = $user->updateInDB(['picture']);
+               if ($this->debug) {
+                  print_r(['id' => $user->getId(),
+                           'picture' => "{$sub}/{$filename}.jpg",
+                           'success' => $success
+                  ]);
+               }
+
+               if (!$success) {
+                  if ($this->debug) {
+                     print_r(false);
+                  }
+                  return false;
+               }
+
+               if ($this->debug) {
+                  print_r("{$sub}/{$filename}.jpg");
+               }
+               return "{$sub}/{$filename}.jpg";
+            }
+            if ($this->debug) {
+               print_r($user->fields["picture"]);
+            }
+            return $user->fields["picture"];
+         }
+      }
+
+      if ($this->debug) {
+         print_r(false);
+      }
+      return false;
+   }
 }

+ 61 - 4
inc/toolbox.class.php

@@ -37,7 +37,7 @@ class PluginSinglesignonToolbox {
    public static function getCallbackUrl($row, $query = []) {
       global $CFG_GLPI;
 
-      $url = $CFG_GLPI['root_doc'] . '/plugins/singlesignon/front/callback.php';
+      $url = Plugin::getPhpDir("singlesignon", false) . '/front/callback.php';
 
       $url .= "/provider/".$row;
 
@@ -94,7 +94,7 @@ class PluginSinglesignonToolbox {
       return $data;
    }
 
-   static public function startsWith($haystack, $needle) {
+   public static function startsWith($haystack, $needle) {
       $length = strlen($needle);
       return (substr($haystack, 0, $length) === $needle);
    }
@@ -108,10 +108,10 @@ class PluginSinglesignonToolbox {
          return null;
       }
 
-      return $CFG_GLPI['root_doc'] . '/plugins/singlesignon/front/picture.send.php?path=' . $path;
+      return PluginSinglesignonToolbox::getBaseURL() . Plugin::getPhpDir("singlesignon", false) . '/front/picture.send.php?path=' . $path;
    }
 
-   static public function savePicture($src, $uniq_prefix = "") {
+   public static function savePicture($src, $uniq_prefix = "") {
 
       if (function_exists('Document::isImage') && !Document::isImage($src)) {
          return false;
@@ -196,4 +196,61 @@ class PluginSinglesignonToolbox {
       $btn .= '</a></span>';
       return $btn;
    }
+
+   /**
+    * Get base URL without query string
+    * @return string
+    */
+   public static function getBaseURL() {
+      global $CFG_GLPI;
+
+      if (!empty($CFG_GLPI['url_base'])) {
+         return $CFG_GLPI['url_base'];
+      }
+
+      $baseURL = "";
+      if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
+         $baseURL = ($_SERVER["HTTP_X_FORWARDED_PROTO"] == "https") ? "https://" : "http://";
+      } else if (isset($_SERVER["HTTPS"])) {
+         $baseURL = ($_SERVER["HTTPS"] == "on") ? "https://" : "http://";
+      } else {
+         $baseURL = "http://";
+      }
+
+      if (isset($_SERVER["HTTP_X_FORWARDED_HOST"])) {
+         $baseURL .= $_SERVER["HTTP_X_FORWARDED_HOST"];
+      } else if (isset($_SERVER["HTTP_X_FORWARDED_HOST"])) {
+         $baseURL .= $_SERVER["HTTP_X_FORWARDED_HOST"];
+      } else {
+         $baseURL .= $_SERVER["SERVER_NAME"];
+      }
+
+      $port = $_SERVER["SERVER_PORT"];
+      if (isset($_SERVER["HTTP_X_FORWARDED_PORT"])) {
+         $port = $_SERVER["HTTP_X_FORWARDED_PORT"];
+      }
+
+      if ($port != "80" && $port != "443") {
+         $baseURL .= ":" . $_SERVER["SERVER_PORT"];
+      }
+      return $baseURL;
+   }
+
+   /**
+    * Get current URL without query string
+    * @return string
+    */
+   public static function getCurrentURL() {
+      $currentURL = PluginSinglesignonToolbox::getBaseURL();
+
+      // $currentURL .= $_SERVER["REQUEST_URI"];
+      // Ignore Query String
+      if (isset($_SERVER["SCRIPT_NAME"])) {
+         $currentURL .= $_SERVER["SCRIPT_NAME"];
+      }
+      if (isset($_SERVER["PATH_INFO"])) {
+         $currentURL .= $_SERVER["PATH_INFO"];
+      }
+      return $currentURL;
+   }
 }

BIN
locales/de_DE.mo


BIN
locales/en_GB.mo


BIN
locales/es_ES.mo


BIN
locales/fr_FR.mo


BIN
locales/pt_BR.mo


+ 26 - 13
locales/pt_BR.po

@@ -8,8 +8,8 @@ msgstr ""
 "Project-Id-Version: singlesignon 1.0.0\n"
 "Report-Msgid-Bugs-To: https://github.com/edgardmessias/glpi-singlesignon/"
 "issues\n"
-"POT-Creation-Date: 2022-06-28 23:01-0300\n"
-"PO-Revision-Date: 2019-04-26 11:04-0300\n"
+"POT-Creation-Date: 2022-06-28 23:02-0300\n"
+"PO-Revision-Date: 2025-01-31 19:31-0300\n"
 "Last-Translator: Edgard Lorraine Messias <edgardmessias@gmail.com>\n"
 "Language-Team: none\n"
 "Language: pt_BR\n"
@@ -17,6 +17,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Generator: Poedit 3.5\n"
 
 #: front/callback.php:15
 msgid "Provider not defined."
@@ -82,11 +83,23 @@ msgstr "URL de Detalhes do Proprietário do Recurso"
 
 #: inc/provider.class.php:145
 msgid "PopupAuth"
-msgstr ""
+msgstr "Autenticação Pop-up"
 
 #: inc/provider.class.php:151
 msgid "SplitDomain"
-msgstr ""
+msgstr "Dividir Domínio"
+
+#: inc/provider.class.php:168
+msgid "IsDefault"
+msgstr "É Padrão"
+
+#: inc/provider.class.php:188
+msgid "Use Email as Login"
+msgstr "Usar E-mail como Login"
+
+#: inc/provider.class.php:191
+msgid "Split Name"
+msgstr "Dividir Nome"
 
 #: inc/provider.class.php:155
 #, fuzzy
@@ -94,8 +107,8 @@ msgid "AuthorizedDomains"
 msgstr "URL de Autorização"
 
 #: inc/provider.class.php:157
-msgid "AuthorizedDomainsTooltip"
-msgstr ""
+msgid "Provide a list of domains allowed to log in through this provider (separated by commas, no spaces)."
+msgstr "Forneça uma lista de domínios permitidos para fazer login por meio deste provedor (separados por vírgulas, sem espaços)."
 
 #: inc/provider.class.php:246
 msgid "Callback URL"
@@ -107,7 +120,7 @@ msgstr "Testar Logon Único"
 
 #: inc/provider.class.php:308
 msgid "A Name is required"
-msgstr "Nome é obrigatório"
+msgstr "O Nome é obrigatório"
 
 #: inc/provider.class.php:314
 #, php-format
@@ -116,15 +129,15 @@ msgstr "O \"%s\" é um tipo inválido"
 
 #: inc/provider.class.php:318
 msgid "A Client ID is required"
-msgstr "ID de cliente é obrigatório"
+msgstr "O ID de cliente é obrigatório"
 
 #: inc/provider.class.php:322
 msgid "A Client Secret is required"
-msgstr "Segredo do Cliente é obrigatório"
+msgstr "O Segredo do Cliente é obrigatório"
 
 #: inc/provider.class.php:327
 msgid "An Authorize URL is required"
-msgstr "URL de Autorização é obrigatório"
+msgstr "A URL de Autorização é obrigatória"
 
 #: inc/provider.class.php:329
 msgid "The Authorize URL is invalid"
@@ -132,7 +145,7 @@ msgstr "A URL de Autorização é inválida"
 
 #: inc/provider.class.php:333
 msgid "An Access Token URL is required"
-msgstr "URL de Token de Acesso é obrigatório"
+msgstr "A URL de Token de Acesso é obrigatória"
 
 #: inc/provider.class.php:335
 msgid "The Access Token URL is invalid"
@@ -140,7 +153,7 @@ msgstr "A URL de Token de Acesso é inválida"
 
 #: inc/provider.class.php:339
 msgid "A Resource Owner Details URL is required"
-msgstr "URL de Detalhes do Proprietário do Recurso é obrigatório"
+msgstr "A URL de Detalhes do Proprietário do Recurso é obrigatória"
 
 #: inc/provider.class.php:341
 msgid "The Resource Owner Details URL is invalid"
@@ -152,7 +165,7 @@ msgstr "Genérico"
 
 #: inc/provider.class.php:553
 msgid "Azure"
-msgstr ""
+msgstr "Azure"
 
 #: inc/provider.class.php:554
 msgid "Facebook"

+ 73 - 60
locales/singlesignon.pot

@@ -1,188 +1,201 @@
 # SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# Copyright (C) YEAR glpi-singlesignon Development Team
 # This file is distributed under the same license as the singlesignon package.
 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
 #
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: singlesignon 1.3.2\n"
-"Report-Msgid-Bugs-To: https://github.com/edgardmessias/glpi-singlesignon/"
-"issues\n"
-"POT-Creation-Date: 2022-06-28 23:02-0300\n"
+"Project-Id-Version: singlesignon\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-02-03 12:56-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"
 "Language: \n"
 "MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Type: text/plain; charset=CHARSET\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: front/callback.php:15
+#: setup.php:33
+#, php-format
+msgid "Please, rename the plugin folder \"%s\" to \"singlesignon\""
+msgstr ""
+
+#: setup.php:67 front/provider.form.php:86 front/provider.form.php:88
+#: front/provider.php:33 front/provider.php:35 inc/preference.class.php:89
+#: inc/provider.class.php:84
+msgid "Single Sign-on"
+msgstr ""
+
+#: setup.php:84
+msgid "This plugin requires GLPI >= 0.85"
+msgstr ""
+
+#: front/callback.php:40
 msgid "Provider not defined."
 msgstr ""
 
-#: front/callback.php:21
+#: front/callback.php:46
 msgid "Provider not found."
 msgstr ""
 
-#: front/callback.php:25
+#: front/callback.php:50
 msgid "Provider not active."
 msgstr ""
 
-#: front/callback.php:115
+#: front/callback.php:108
 msgid "Automatic redirection, else click"
 msgstr ""
 
-#: front/provider.form.php:61 front/provider.form.php:63 front/provider.php:8
-#: front/provider.php:10 inc/preference.class.php:64 inc/provider.class.php:59
-#: setup.php:40
-msgid "Single Sign-on"
-msgstr ""
-
-#: inc/preference.class.php:147 inc/provider.class.php:52
+#: inc/preference.class.php:172 inc/provider.class.php:77
 msgid "Single Sign-on Provider"
 msgstr ""
 
-#: inc/preference.class.php:172
+#: inc/preference.class.php:197
 msgid "Linked accounts"
 msgstr ""
 
-#: inc/provider.class.php:100
+#: inc/provider.class.php:125
 msgid "SSO Type"
 msgstr ""
 
-#: inc/provider.class.php:108 inc/provider.class.php:445
+#: inc/provider.class.php:133 inc/provider.class.php:472
 msgid "Client ID"
 msgstr ""
 
-#: inc/provider.class.php:110 inc/provider.class.php:453
+#: inc/provider.class.php:135 inc/provider.class.php:480
 msgid "Client Secret"
 msgstr ""
 
-#: inc/provider.class.php:121 inc/provider.class.php:461
+#: inc/provider.class.php:146 inc/provider.class.php:488
 msgid "Scope"
 msgstr ""
 
-#: inc/provider.class.php:123 inc/provider.class.php:469
+#: inc/provider.class.php:148 inc/provider.class.php:496
 msgid "Extra Options"
 msgstr ""
 
-#: inc/provider.class.php:128 inc/provider.class.php:477
+#: inc/provider.class.php:153 inc/provider.class.php:504
 msgid "Authorize URL"
 msgstr ""
 
-#: inc/provider.class.php:133 inc/provider.class.php:485
+#: inc/provider.class.php:158 inc/provider.class.php:512
 msgid "Access Token URL"
 msgstr ""
 
-#: inc/provider.class.php:138 inc/provider.class.php:493
+#: inc/provider.class.php:163 inc/provider.class.php:520
 msgid "Resource Owner Details URL"
 msgstr ""
 
-#: inc/provider.class.php:145
+#: inc/provider.class.php:168
+msgid "IsDefault"
+msgstr ""
+
+#: inc/provider.class.php:170
 msgid "PopupAuth"
 msgstr ""
 
-#: inc/provider.class.php:151
+#: inc/provider.class.php:176
 msgid "SplitDomain"
 msgstr ""
 
-#: inc/provider.class.php:155
+#: inc/provider.class.php:180
 msgid "AuthorizedDomains"
 msgstr ""
 
-#: inc/provider.class.php:157
-msgid "AuthorizedDomainsTooltip"
+#: inc/provider.class.php:182
+msgid ""
+"Provide a list of domains allowed to log in through this provider (separated "
+"by commas, no spaces)."
+msgstr ""
+
+#: inc/provider.class.php:188
+msgid "Use Email as Login"
 msgstr ""
 
-#: inc/provider.class.php:246
+#: inc/provider.class.php:191
+msgid "Split Name"
+msgstr ""
+
+#: inc/provider.class.php:279
 msgid "Callback URL"
 msgstr ""
 
-#: inc/provider.class.php:250
+#: inc/provider.class.php:283
 msgid "Test Single Sign-on"
 msgstr ""
 
-#: inc/provider.class.php:308
+#: inc/provider.class.php:342
 msgid "A Name is required"
 msgstr ""
 
-#: inc/provider.class.php:314
+#: inc/provider.class.php:348
 #, php-format
 msgid "The \"%s\" is a Invalid type"
 msgstr ""
 
-#: inc/provider.class.php:318
+#: inc/provider.class.php:352
 msgid "A Client ID is required"
 msgstr ""
 
-#: inc/provider.class.php:322
+#: inc/provider.class.php:356
 msgid "A Client Secret is required"
 msgstr ""
 
-#: inc/provider.class.php:327
+#: inc/provider.class.php:361
 msgid "An Authorize URL is required"
 msgstr ""
 
-#: inc/provider.class.php:329
+#: inc/provider.class.php:363
 msgid "The Authorize URL is invalid"
 msgstr ""
 
-#: inc/provider.class.php:333
+#: inc/provider.class.php:367
 msgid "An Access Token URL is required"
 msgstr ""
 
-#: inc/provider.class.php:335
+#: inc/provider.class.php:369
 msgid "The Access Token URL is invalid"
 msgstr ""
 
-#: inc/provider.class.php:339
+#: inc/provider.class.php:373
 msgid "A Resource Owner Details URL is required"
 msgstr ""
 
-#: inc/provider.class.php:341
+#: inc/provider.class.php:375
 msgid "The Resource Owner Details URL is invalid"
 msgstr ""
 
-#: inc/provider.class.php:552
+#: inc/provider.class.php:597
 msgid "Generic"
 msgstr ""
 
-#: inc/provider.class.php:553
+#: inc/provider.class.php:598
 msgid "Azure"
 msgstr ""
 
-#: inc/provider.class.php:554
+#: inc/provider.class.php:599
 msgid "Facebook"
 msgstr ""
 
-#: inc/provider.class.php:555
+#: inc/provider.class.php:600
 msgid "GitHub"
 msgstr ""
 
-#: inc/provider.class.php:556
+#: inc/provider.class.php:601
 msgid "Google"
 msgstr ""
 
-#: inc/provider.class.php:557
+#: inc/provider.class.php:602
 msgid "Instagram"
 msgstr ""
 
-#: inc/provider.class.php:558
+#: inc/provider.class.php:603
 msgid "LinkdeIn"
 msgstr ""
 
-#: inc/toolbox.class.php:166
+#: inc/toolbox.class.php:195
 #, php-format
 msgid "Login with %s"
 msgstr ""
-
-#: setup.php:8
-#, php-format
-msgid "Please, rename the plugin folder \"%s\" to \"singlesignon\""
-msgstr ""
-
-#: setup.php:57
-msgid "This plugin requires GLPI >= 0.85"
-msgstr ""

+ 7 - 0
transifex.yml

@@ -0,0 +1,7 @@
+git:
+  filters:
+    - filter_type: file
+      file_format: PO
+      source_file: locales/singlesignon.pot
+      source_language: en
+      translation_files_expression: locales/<lang>.po