Browse Source

OAuth (part 10 draft)

If cURL is disabled, it will work through files/streams with allow_url_fopen=ON.
Visman 2 years ago
parent
commit
39dce53001

+ 4 - 1
app/Models/Pages/Admin/Providers.php

@@ -23,7 +23,10 @@ class Providers extends Admin
      */
     protected function mDisabled(): void
     {
-        if (! \extension_loaded('curl')) {
+        if (
+            ! \extension_loaded('curl')
+            && ! \filter_var(\ini_get('allow_url_fopen'), \FILTER_VALIDATE_BOOL)
+        ) {
             $this->fIswev = [FORK_MESS_ERR, 'cURL disabled'];
         } elseif (1 !== $this->c->config->b_oauth_allow) {
             $this->fIswev = [FORK_MESS_WARN, ['OAuth authorization disabled', $this->c->Router->link('AdminOptions', ['#' => 'id-fs-registration'])]];

+ 165 - 32
app/Models/Provider/Driver.php

@@ -143,61 +143,194 @@ abstract class Driver extends Model
     {
         $this->access_token = '';
 
-        $params = [
-            'grant_type'    => 'authorization_code',
-            'client_id'     => $this->client_id,
-            'client_secret' => $this->client_secret,
-            'code'          => $this->code,
-            'redirect_uri'  => $this->linkCallback,
+        $options = [
+            'headers' => [
+                'Accept: application/json',
+                'Content-type: application/x-www-form-urlencoded',
+            ],
+            'form_params' => [
+                'grant_type'    => 'authorization_code',
+                'client_id'     => $this->client_id,
+                'client_secret' => $this->client_secret,
+                'code'          => $this->code,
+                'redirect_uri'  => $this->linkCallback,
+            ],
         ];
 
-        if (empty($ch = \curl_init($this->tokenURL))) {
-            $this->error     = 'cURL error';
-            $this->curlError = \curl_error($ch);
+        $response = $this->request('POST', $this->tokenURL, $options);
+
+        if (isset($response['access_token'])) {
+            $this->access_token = $response['access_token'];
+
+            return true;
+
+        } elseif (\is_array($response)) {
+            $error = $response['error_description'] ?? ($response['error'] ?? null);
+
+            if (! \is_string($error)) {
+                $error = 'undefined error';
+            }
+
+            $this->error = ['Token error: %s', $error];
+        }
+
+        return false;
+    }
+
+    /**
+     * Обменивается данными c сервером OAuth
+     * Ответ пытается преобразовать в массив
+     */
+    protected function request(string $method, string $url, array $options): array|false
+    {
+        $result = null;
+
+        //преобразовать массив переменных запроса в строку запроса
+        if (\is_array($options['query'] ?? null)) {
+            $options['query'] = \http_build_query($options['query']);
+        }
+        // дополнить url строкой запроса
+        if (\is_string($options['query'] ?? null)) {
+            $url .= (false === \strpos($url, '?') ? '?' : '&') . $options['query'];
+
+            unset($options['query']);
+        }
+
+        if (\extension_loaded('curl')) {
+            $result = $this->curlRequest($method, $url, $options);
+        } elseif (\filter_var(\ini_get('allow_url_fopen'), \FILTER_VALIDATE_BOOL)) {
+            $result = $this->streamRequest($method, $url, $options);
+        }
+
+        if (null === $result) {
+            $this->error = 'No cURL and allow_url_fopen OFF';
+
+            return false;
+
+        } elseif (\is_string($result)) {
+            if (\str_starts_with($this->respContentType, 'application/json')) {
+                $data = \json_decode($result, true, 20, self::JSON_OPTIONS);
+
+                if (\is_array($data)) {
+                    return $this->c->Secury->replInvalidChars($data);
+                }
+
+                $this->error ??= 'Bad json';
+            }
+
+            $this->error ??= "Bad Content-type: {$this->respContentType}";
+        }
+
+        return false;
+    }
+
+    /**
+     * Отправляет/получает данные через cURL
+     */
+    protected function curlRequest(string $method, string $url, array $options): string|false
+    {
+        $ch = \curl_init($url);
+
+        if (! $ch) {
+            $this->error = "Failed cURL init for {$url}";
 
             return false;
         }
 
+        switch ($method) {
+            case 'POST':
+                \curl_setopt($ch, \CURLOPT_POST, true);
+
+                if (\is_array($options['form_params'] ?? null)) {
+                    \curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query($options['form_params']));
+                }
+
+                break;
+            default:
+                \curl_setopt($ch, \CURLOPT_HTTPGET, true);
+
+                break;
+        }
+
         \curl_setopt($ch, \CURLOPT_MAXREDIRS, 10);
         \curl_setopt($ch, \CURLOPT_TIMEOUT, 10);
-        \curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Accept: application/json']);
-        \curl_setopt($ch, \CURLOPT_POST, true);
-        \curl_setopt($ch, \CURLOPT_POSTFIELDS, \http_build_query($params));
         \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
         \curl_setopt($ch, \CURLOPT_HEADER, false);
+        \curl_setopt($ch, \CURLOPT_USERAGENT, "ForkBB (Client ID: {$this->client_id})");
+
+        if (\is_array($options['headers'] ?? null)) {
+            \curl_setopt($ch, \CURLOPT_HTTPHEADER, $options['headers']);
+        }
 
         $result = \curl_exec($ch);
 
+        if (false === $result) {
+            $this->error = 'cURL error: ' . \curl_error($ch);
+        } else {
+            $this->respContentType = \curl_getinfo($ch, \CURLINFO_CONTENT_TYPE);
+            $this->respHttpCode    = \curl_getinfo($ch, \CURLINFO_RESPONSE_CODE);
+        }
+
         \curl_close($ch);
 
-        if (false === $result) {
-            $this->error     = 'cURL error';
-            $this->curlError = \curl_error($ch);
+        return $result;
+    }
 
-            return false;
-        }
+    /**
+     * Отправляет/получает данные через file_get_contents()
+     */
+    protected function streamRequest(string $method, string $url, array $options): string|false
+    {
+        $http = [
+            'max_redirects' => 10,
+            'timeout'       => 10,
+            'user_agent'    => "ForkBB (Client ID: {$this->client_id})",
+        ];
 
-        if (
-            ! isset($result[1])
-            || '{' !== $result[0]
-            || '}' !== $result[-1]
-            || ! \is_array($data = \json_decode($result, true, 20, self::JSON_OPTIONS))
-            || ! isset($data['access_token'])
-        ) {
-            $error = $data['error_description'] ?? ($data['error'] ?? null);
+        switch ($method) {
+            case 'POST':
+                $http['method'] = 'POST';
 
-            if (! \is_string($error)) {
-                $error = 'undefined error';
-            }
+                if (\is_array($options['form_params'] ?? null)) {
+                    $http['content'] = \http_build_query($options['form_params']);
+                }
 
-            $this->error = ['Token error: %s', $error];
+                break;
+            default:
+                $http['method'] = 'GET';
 
-            return false;
+                break;
         }
 
-        $this->access_token = $data['access_token'];
+        if (\is_array($options['headers'] ?? null)) {
+            $http['header'] = $options['headers'];
+        }
 
-        return true;
+        $context = \stream_context_create(['http' => $http]);
+        $result  = @\file_get_contents($url, false, $context);
+
+        if (false === $result) {
+            $this->error = "Failed file_get_contents for {$url}";
+        } else {
+            $this->respContentType = $this->parseHeader($http_response_header, 'Content-Type:\s*(.+)');
+            $this->respHttpCode    = (int) $this->parseHeader($http_response_header, 'HTTP/[0-9.]+\s+([0-9]+)');
+        }
+
+        return $result;
+    }
+
+    /**
+     * Достает по шаблону значение нужного заголовка из списка
+     */
+    protected function parseHeader(array $headers, string $pattern): string
+    {
+        while ($header = \array_pop($headers)) {
+            if (\preg_match('%^' . $pattern . '%i', $header, $matches)) {
+                return \trim($matches[1]);
+            }
+        }
+
+        return '';
     }
 
     /**

+ 11 - 40
app/Models/Provider/Driver/GitHub.php

@@ -30,53 +30,24 @@ class GitHub extends Driver
     {
         $this->userInfo = [];
 
-        $headers = [
-            'Accept: application/json',
-            "Authorization: Bearer {$this->access_token}",
-            "User-Agent: ForkBB (Client ID: {$this->client_id})",
+        $options = [
+            'headers' => [
+                'Accept: application/json',
+                "Authorization: Bearer {$this->access_token}",
+            ],
         ];
 
-        if (empty($ch = \curl_init($this->userURL))) {
-            $this->error     = 'cURL error';
-            $this->curlError = \curl_error($ch);
+        $response = $this->request('GET', $this->userURL, $options);
 
-            return false;
-        }
-
-        \curl_setopt($ch, \CURLOPT_MAXREDIRS, 10);
-        \curl_setopt($ch, \CURLOPT_TIMEOUT, 10);
-        \curl_setopt($ch, \CURLOPT_HTTPHEADER, $headers);
-        \curl_setopt($ch, \CURLOPT_HTTPGET, true);
-//        \curl_setopt($ch, \CURLOPT_POST, false);
-        \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true);
-        \curl_setopt($ch, \CURLOPT_HEADER, false);
-
-        $result = \curl_exec($ch);
-
-        \curl_close($ch);
-
-        if (false === $result) {
-            $this->error     = 'cURL error';
-            $this->curlError = \curl_error($ch);
+        if (! empty($response['id'])) {
+            $this->userInfo = $response;
 
-            return false;
-        }
-
-        if (
-            ! isset($result[1])
-            || '{' !== $result[0]
-            || '}' !== $result[-1]
-            || ! \is_array($userInfo = \json_decode($result, true, 20, self::JSON_OPTIONS))
-            || empty($userInfo['id'])
-        ) {
+            return true;
+        } elseif (\is_array($response)) {
             $this->error = 'User error';
-
-            return false;
         }
 
-        $this->userInfo = $this->c->Secury->replInvalidChars($userInfo);
-
-        return true;
+        return false;
     }
 
     /**

+ 4 - 1
app/Models/Provider/Providers.php

@@ -96,7 +96,10 @@ class Providers extends Manager
      */
     public function active(): array
     {
-        if (! \extension_loaded('curl')) {
+        if (
+            ! \extension_loaded('curl')
+            && ! \filter_var(\ini_get('allow_url_fopen'), \FILTER_VALIDATE_BOOL)
+        ) {
             return [];
         }
 

+ 1 - 1
app/lang/en/admin_providers.po

@@ -70,7 +70,7 @@ msgid "Token error: %s"
 msgstr "Access token request failed: %s."
 
 msgid "cURL disabled"
-msgstr "The cURL extension is disabled in PHP."
+msgstr "The cURL extension is disabled in PHP and allow_url_fopen = OFF."
 
 msgid "cURL error"
 msgstr "cURL error."

+ 1 - 1
app/lang/ru/admin_providers.po

@@ -70,7 +70,7 @@ msgid "Token error: %s"
 msgstr "Ошибка запроса токена доступа: %s."
 
 msgid "cURL disabled"
-msgstr "Расширение cURL отключено в PHP."
+msgstr "Расширение cURL отключено в PHP и allow_url_fopen = OFF."
 
 msgid "cURL error"
 msgstr "Ошибка cURL."