From dd8a4a0082f54e777b25113a2fd4062626d7dd56 Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Thu, 12 May 2022 14:40:55 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B03169=20-=20Add=20feature=20to=20connect?= =?UTF-8?q?=20Gsuite=20mail=20box=20with=20OAuth=20N=C2=B02504=20-=20Add?= =?UTF-8?q?=20feature=20to=20connect=20Office=20mail=20box=20with=20OAuth2?= =?UTF-8?q?=20for=20Microsoft=20Graph=20N=C2=B05102=20-=20Allow=20to=20sen?= =?UTF-8?q?d=20emails=20(eg.=20notifications)=20using=20GSuite=20SMTP=20an?= =?UTF-8?q?d=20OAuth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 4 + composer.lock | 1177 ++++++++++++++++- lib/autoload.php | 5 - lib/composer/ClassLoader.php | 2 +- lib/composer/autoload_classmap.php | 3 +- lib/composer/autoload_files.php | 8 +- lib/composer/autoload_namespaces.php | 2 +- lib/composer/autoload_psr4.php | 2 +- lib/composer/autoload_real.php | 30 +- lib/composer/autoload_static.php | 7 +- lib/composer/include_paths.php | 2 +- pages/oauth/ajax.wizard.php | 63 + pages/oauth/landing.php | 26 + pages/oauth/wizard.php | 159 +++ .../OAuth/OAuthWizardController.php | 48 + .../Client/OAuth/IOAuthClientProvider.php | 5 + .../OAuth/IOAuthClientResultDisplay.php | 8 + .../OAuth/OAuthClientProviderAbstract.php | 163 +++ .../Client/OAuth/OAuthClientProviderAzure.php | 46 + .../OAuth/OAuthClientProviderFactory.php | 73 + .../OAuth/OAuthClientProviderGoogle.php | 41 + .../OAuth/OAuthClientResultDisplayConf.php | 40 + .../Client/Smtp/SmtpOAuthLogin.php | 123 ++ sources/Core/Email/EmailFactory.php | 6 + sources/Core/Email/EmailLaminas.php | 643 +++++++++ sources/Core/Email/EmailSwiftMailer.php | 550 ++++++++ .../pages/backoffice/oauth/Wizard.html.twig | 48 + 27 files changed, 3254 insertions(+), 30 deletions(-) create mode 100644 pages/oauth/ajax.wizard.php create mode 100644 pages/oauth/landing.php create mode 100644 pages/oauth/wizard.php create mode 100644 sources/Controller/OAuth/OAuthWizardController.php create mode 100644 sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php create mode 100644 sources/Core/Authentication/Client/OAuth/IOAuthClientResultDisplay.php create mode 100644 sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php create mode 100644 sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php create mode 100644 sources/Core/Authentication/Client/OAuth/OAuthClientProviderFactory.php create mode 100644 sources/Core/Authentication/Client/OAuth/OAuthClientProviderGoogle.php create mode 100644 sources/Core/Authentication/Client/OAuth/OAuthClientResultDisplayConf.php create mode 100644 sources/Core/Authentication/Client/Smtp/SmtpOAuthLogin.php create mode 100644 sources/Core/Email/EmailFactory.php create mode 100644 sources/Core/Email/EmailLaminas.php create mode 100644 sources/Core/Email/EmailSwiftMailer.php create mode 100644 templates/pages/backoffice/oauth/Wizard.html.twig diff --git a/composer.json b/composer.json index 57599b9e1..3b32231e5 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,9 @@ "ext-mysqli": "*", "ext-soap": "*", "combodo/tcpdf": "~6.4.4", + "laminas/laminas-mail": "^2.11", + "laminas/laminas-servicemanager": "^3.5", + "league/oauth2-google": "^3.0", "nikic/php-parser": "~4.13.2", "pear/archive_tar": "~1.4.14", "pelago/emogrifier": "~3.1.0", @@ -21,6 +24,7 @@ "symfony/framework-bundle": "~3.4.47", "symfony/twig-bundle": "~3.4.47", "symfony/yaml": "~3.4.47", + "thenetworg/oauth2-azure": "^2.0", "twig/twig": "~1.42.5" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 7791458dc..f753cd18b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2e22d4aefde79b10575e4de746f22f32", + "content-hash": "16a1d6529177e5ab83a163236d9602ea", "packages": [ { "name": "combodo/tcpdf", @@ -84,6 +84,42 @@ ], "time": "2022-03-10T14:36:39+00:00" }, + { + "name": "container-interop/container-interop", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "support": { + "issues": "https://github.com/container-interop/container-interop/issues", + "source": "https://github.com/container-interop/container-interop/tree/master" + }, + "abandoned": "psr/container", + "time": "2017-02-14T19:40:03+00:00" + }, { "name": "doctrine/lexer", "version": "1.0.2", @@ -216,6 +252,941 @@ ], "time": "2020-12-29T14:50:06+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v5.5.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "83b609028194aa042ea33b5af2d41a7427de80e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/83b609028194aa042ea33b5af2d41a7427de80e6", + "reference": "83b609028194aa042ea33b5af2d41a7427de80e6", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4.8 <=9" + }, + "suggest": { + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v5.5.1" + }, + "time": "2021-11-08T20:18:51+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.5.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.6.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5" + }, + "time": "2020-06-16T21:01:06+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:56:57+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "337e3ad8e5716c15f9657bd214d16cc5e69df268" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/337e3ad8e5716c15f9657bd214d16cc5e69df268", + "reference": "337e3ad8e5716c15f9657bd214d16cc5e69df268", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.8.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2022-03-20T21:51:18+00:00" + }, + { + "name": "laminas/laminas-loader", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-loader.git", + "reference": "5d01c2c237ae9e68bec262f339947e2ea18979bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-loader/zipball/5d01c2c237ae9e68bec262f339947e2ea18979bc", + "reference": "5d01c2c237ae9e68bec262f339947e2ea18979bc", + "shasum": "" + }, + "require": { + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^5.6 || ^7.0" + }, + "replace": { + "zendframework/zend-loader": "self.version" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6.x-dev", + "dev-develop": "2.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Loader\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Autoloading and plugin loading strategies", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "loader" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-loader/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-loader/issues", + "rss": "https://github.com/laminas/laminas-loader/releases.atom", + "source": "https://github.com/laminas/laminas-loader" + }, + "time": "2019-12-31T17:18:27+00:00" + }, + { + "name": "laminas/laminas-mail", + "version": "2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-mail.git", + "reference": "7f674afeb38100b1869ce8e56bf2ec3cba3c679c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/7f674afeb38100b1869ce8e56bf2ec3cba3c679c", + "reference": "7f674afeb38100b1869ce8e56bf2ec3cba3c679c", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "laminas/laminas-loader": "^2.5", + "laminas/laminas-mime": "^2.5", + "laminas/laminas-stdlib": "^2.7 || ^3.0", + "laminas/laminas-validator": "^2.10.2", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^5.6 || ^7.0", + "true/punycode": "^2.1" + }, + "replace": { + "zendframework/zend-mail": "^2.10.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-config": "^2.6", + "laminas/laminas-crypt": "^2.6 || ^3.0", + "laminas/laminas-servicemanager": "^2.7.10 || ^3.3.1", + "phpunit/phpunit": "^5.7.25 || ^6.4.4 || ^7.1.4" + }, + "suggest": { + "laminas/laminas-crypt": "Crammd5 support in SMTP Auth", + "laminas/laminas-servicemanager": "^2.7.10 || ^3.3.1 when using SMTP to deliver messages" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.11.x-dev", + "dev-develop": "2.12.x-dev" + }, + "laminas": { + "component": "Laminas\\Mail", + "config-provider": "Laminas\\Mail\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Mail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Provides generalized functionality to compose and send both text and MIME-compliant multipart e-mail messages", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "mail" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-mail/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-mail/issues", + "rss": "https://github.com/laminas/laminas-mail/releases.atom", + "source": "https://github.com/laminas/laminas-mail" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-07-28T21:17:48+00:00" + }, + { + "name": "laminas/laminas-mime", + "version": "2.7.4", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-mime.git", + "reference": "e45a7d856bf7b4a7b5bd00d6371f9961dc233add" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-mime/zipball/e45a7d856bf7b4a7b5bd00d6371f9961dc233add", + "reference": "e45a7d856bf7b4a7b5bd00d6371f9961dc233add", + "shasum": "" + }, + "require": { + "laminas/laminas-stdlib": "^2.7 || ^3.0", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^5.6 || ^7.0" + }, + "replace": { + "zendframework/zend-mime": "^2.7.2" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-mail": "^2.6", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20" + }, + "suggest": { + "laminas/laminas-mail": "Laminas\\Mail component" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7.x-dev", + "dev-develop": "2.8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Mime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Create and parse MIME messages and parts", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "mime" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-mime/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-mime/issues", + "rss": "https://github.com/laminas/laminas-mime/releases.atom", + "source": "https://github.com/laminas/laminas-mime" + }, + "time": "2020-03-29T13:12:07+00:00" + }, + { + "name": "laminas/laminas-servicemanager", + "version": "3.5.2", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-servicemanager.git", + "reference": "0669e1eec8d9f61e35a5bc5012796d49f418b259" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/0669e1eec8d9f61e35a5bc5012796d49f418b259", + "reference": "0669e1eec8d9f61e35a5bc5012796d49f418b259", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.2", + "laminas/laminas-stdlib": "^3.2.1", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^5.6 || ^7.0", + "psr/container": "^1.0" + }, + "provide": { + "container-interop/container-interop-implementation": "^1.2", + "psr/container-implementation": "^1.0" + }, + "replace": { + "zendframework/zend-servicemanager": "^3.4.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "mikey179/vfsstream": "^1.6.5", + "ocramius/proxy-manager": "^1.0 || ^2.0", + "phpbench/phpbench": "^0.13.0", + "phpunit/phpunit": "^5.7.25 || ^6.4.4" + }, + "suggest": { + "laminas/laminas-stdlib": "laminas-stdlib ^2.5 if you wish to use the MergeReplaceKey or MergeRemoveKey features in Config instances", + "ocramius/proxy-manager": "ProxyManager 1.* to handle lazy initialization of services" + }, + "bin": [ + "bin/generate-deps-for-config-factory", + "bin/generate-factory-for-class" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev", + "dev-develop": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Laminas\\ServiceManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Factory-Driven Dependency Injection Container", + "homepage": "https://laminas.dev", + "keywords": [ + "PSR-11", + "dependency-injection", + "di", + "dic", + "laminas", + "service-manager", + "servicemanager" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-servicemanager/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-servicemanager/issues", + "rss": "https://github.com/laminas/laminas-servicemanager/releases.atom", + "source": "https://github.com/laminas/laminas-servicemanager" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-01-17T16:54:43+00:00" + }, + { + "name": "laminas/laminas-stdlib", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-stdlib.git", + "reference": "2b18347625a2f06a1a485acfbc870f699dbe51c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/2b18347625a2f06a1a485acfbc870f699dbe51c6", + "reference": "2b18347625a2f06a1a485acfbc870f699dbe51c6", + "shasum": "" + }, + "require": { + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^5.6 || ^7.0" + }, + "replace": { + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "phpbench/phpbench": "^0.13", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev", + "dev-develop": "3.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "SPL extensions, array utilities, error handlers, and more", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "stdlib" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-stdlib/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-stdlib/issues", + "rss": "https://github.com/laminas/laminas-stdlib/releases.atom", + "source": "https://github.com/laminas/laminas-stdlib" + }, + "time": "2019-12-31T17:51:15+00:00" + }, + { + "name": "laminas/laminas-validator", + "version": "2.12.2", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-validator.git", + "reference": "0813f234812d9fa9058b6da39eb13dedc90227db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/0813f234812d9fa9058b6da39eb13dedc90227db", + "reference": "0813f234812d9fa9058b6da39eb13dedc90227db", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.1", + "laminas/laminas-stdlib": "^3.2.1", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^5.6 || ^7.0" + }, + "replace": { + "zendframework/zend-validator": "self.version" + }, + "require-dev": { + "laminas/laminas-cache": "^2.6.1", + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-config": "^2.6", + "laminas/laminas-db": "^2.7", + "laminas/laminas-filter": "^2.6", + "laminas/laminas-http": "^2.5.4", + "laminas/laminas-i18n": "^2.6", + "laminas/laminas-math": "^2.6", + "laminas/laminas-servicemanager": "^2.7.5 || ^3.0.3", + "laminas/laminas-session": "^2.8", + "laminas/laminas-uri": "^2.5", + "phpunit/phpunit": "^6.0.8 || ^5.7.15", + "psr/http-message": "^1.0" + }, + "suggest": { + "laminas/laminas-db": "Laminas\\Db component, required by the (No)RecordExists validator", + "laminas/laminas-filter": "Laminas\\Filter component, required by the Digits validator", + "laminas/laminas-i18n": "Laminas\\I18n component to allow translation of validation error messages", + "laminas/laminas-i18n-resources": "Translations of validator messages", + "laminas/laminas-math": "Laminas\\Math component, required by the Csrf validator", + "laminas/laminas-servicemanager": "Laminas\\ServiceManager component to allow using the ValidatorPluginManager and validator chains", + "laminas/laminas-session": "Laminas\\Session component, ^2.8; required by the Csrf validator", + "laminas/laminas-uri": "Laminas\\Uri component, required by the Uri and Sitemap\\Loc validators", + "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.12.x-dev", + "dev-develop": "2.13.x-dev" + }, + "laminas": { + "component": "Laminas\\Validator", + "config-provider": "Laminas\\Validator\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Validator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Validation classes for a wide range of domains, and the ability to chain validators to create complex validation criteria", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "validator" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-validator/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-validator/issues", + "rss": "https://github.com/laminas/laminas-validator/releases.atom", + "source": "https://github.com/laminas/laminas-validator" + }, + "time": "2019-12-31T17:57:44+00:00" + }, + { + "name": "laminas/laminas-zendframework-bridge", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-zendframework-bridge.git", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\ZendFrameworkBridge" + } + }, + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ZendFrameworkBridge\\": "src//" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "keywords": [ + "ZendFramework", + "autoloading", + "laminas", + "zf" + ], + "support": { + "forum": "https://discourse.laminas.dev/", + "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues", + "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom", + "source": "https://github.com/laminas/laminas-zendframework-bridge" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-09-14T14:23:00+00:00" + }, + { + "name": "league/oauth2-client", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "2334c249907190c132364f5dae0287ab8666aa19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/2334c249907190c132364f5dae0287ab8666aa19", + "reference": "2334c249907190c132364f5dae0287ab8666aa19", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "paragonie/random_compat": "^1 || ^2 || ^9.99", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", + "squizlabs/php_codesniffer": "^2.3 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.6.1" + }, + "time": "2021-12-22T16:42:49+00:00" + }, + { + "name": "league/oauth2-google", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-google.git", + "reference": "6b79441f244040760bed5fdcd092a2bda7cf34c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-google/zipball/6b79441f244040760bed5fdcd092a2bda7cf34c6", + "reference": "6b79441f244040760bed5fdcd092a2bda7cf34c6", + "shasum": "" + }, + "require": { + "league/oauth2-client": "^2.0" + }, + "require-dev": { + "eloquent/phony-phpunit": "^2.0", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^6.0", + "squizlabs/php_codesniffer": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "http://shadowhand.me" + } + ], + "description": "Google OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "keywords": [ + "Authentication", + "authorization", + "client", + "google", + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-google/issues", + "source": "https://github.com/thephpleague/oauth2-google/tree/3.0.4" + }, + "time": "2021-01-27T16:09:03+00:00" + }, { "name": "nikic/php-parser", "version": "v4.13.2", @@ -720,6 +1691,59 @@ ], "time": "2017-02-14T16:28:37+00:00" }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, { "name": "psr/log", "version": "1.1.2", @@ -815,6 +1839,50 @@ ], "time": "2017-10-23T01:57:42+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "scssphp/scssphp", "version": "1.0.6", @@ -3188,6 +4256,111 @@ ], "time": "2020-10-24T10:57:07+00:00" }, + { + "name": "thenetworg/oauth2-azure", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/TheNetworg/oauth2-azure.git", + "reference": "2649422a0dc74af32d21d9d738d37abcd5b03998" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TheNetworg/oauth2-azure/zipball/2649422a0dc74af32d21d9d738d37abcd5b03998", + "reference": "2649422a0dc74af32d21d9d738d37abcd5b03998", + "shasum": "" + }, + "require": { + "firebase/php-jwt": "~3.0||~4.0||~5.0", + "league/oauth2-client": "~2.0", + "php": "^5.6|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TheNetworg\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Hajek", + "email": "jan.hajek@thenetw.org", + "homepage": "https://thenetw.org" + } + ], + "description": "Azure Active Directory OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "keywords": [ + "SSO", + "aad", + "authorization", + "azure", + "azure active directory", + "client", + "microsoft", + "oauth", + "oauth2", + "windows azure" + ], + "support": { + "issues": "https://github.com/TheNetworg/oauth2-azure/issues", + "source": "https://github.com/TheNetworg/oauth2-azure/tree/v2.0.1" + }, + "time": "2021-01-11T12:20:12+00:00" + }, + { + "name": "true/punycode", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/true/php-punycode.git", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.7", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TrueBV\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Renan Gonçalves", + "email": "renan.saddam@gmail.com" + } + ], + "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", + "homepage": "https://github.com/true/php-punycode", + "keywords": [ + "idna", + "punycode" + ], + "support": { + "issues": "https://github.com/true/php-punycode/issues", + "source": "https://github.com/true/php-punycode/tree/master" + }, + "time": "2016-11-16T10:37:54+00:00" + }, { "name": "twig/twig", "version": "v1.42.5", @@ -3501,5 +4674,5 @@ "platform-overrides": { "php": "7.0.8" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.1.0" } diff --git a/lib/autoload.php b/lib/autoload.php index 64168f99a..79c1600b5 100644 --- a/lib/autoload.php +++ b/lib/autoload.php @@ -2,11 +2,6 @@ // autoload.php @generated by Composer -if (PHP_VERSION_ID < 50600) { - echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; - exit(1); -} - require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b::getLoader(); diff --git a/lib/composer/ClassLoader.php b/lib/composer/ClassLoader.php index afef3fa2a..0cd6055d1 100644 --- a/lib/composer/ClassLoader.php +++ b/lib/composer/ClassLoader.php @@ -149,7 +149,7 @@ class ClassLoader /** * @return string[] Array of classname => path - * @psalm-return array + * @psalm-var array */ public function getClassMap() { diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 3ddc888cc..11766f30e 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -2,7 +2,7 @@ // autoload_classmap.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( @@ -150,6 +150,7 @@ return array( 'Combodo\\iTop\\Application\\TwigBase\\Twig\\TwigHelper' => $baseDir . '/sources/application/TwigBase/Twig/TwigHelper.php', 'Combodo\\iTop\\Composer\\iTopComposer' => $baseDir . '/sources/Composer/iTopComposer.php', 'Combodo\\iTop\\Controller\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.php', + 'Combodo\\iTop\\Controller\\OAuth\\OAuthWizardController' => $baseDir . '/sources/Controller/OAuth/OAuthWizardController.php', 'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php', 'Combodo\\iTop\\DesignElement' => $baseDir . '/core/designdocument.class.inc.php', 'Combodo\\iTop\\TwigExtension' => $baseDir . '/application/twigextension.class.inc.php', diff --git a/lib/composer/autoload_files.php b/lib/composer/autoload_files.php index 4e28b93b7..af2e18883 100644 --- a/lib/composer/autoload_files.php +++ b/lib/composer/autoload_files.php @@ -2,20 +2,20 @@ // autoload_files.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', - '5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php', '023d27dca8066ef29e6739335ea73bad' => $vendorDir . '/symfony/polyfill-php70/bootstrap.php', + '32dcc8afd4335739640db7d200c1971d' => $vendorDir . '/symfony/polyfill-apcu/bootstrap.php', + '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', 'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', - '32dcc8afd4335739640db7d200c1971d' => $vendorDir . '/symfony/polyfill-apcu/bootstrap.php', 'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php', - '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', '2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php', ); diff --git a/lib/composer/autoload_namespaces.php b/lib/composer/autoload_namespaces.php index e6117c750..d12922d08 100644 --- a/lib/composer/autoload_namespaces.php +++ b/lib/composer/autoload_namespaces.php @@ -2,7 +2,7 @@ // autoload_namespaces.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php index 7d6039aea..28fab5d05 100644 --- a/lib/composer/autoload_psr4.php +++ b/lib/composer/autoload_psr4.php @@ -2,7 +2,7 @@ // autoload_psr4.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/lib/composer/autoload_real.php b/lib/composer/autoload_real.php index 752e35fbd..661cd2543 100644 --- a/lib/composer/autoload_real.php +++ b/lib/composer/autoload_real.php @@ -25,20 +25,33 @@ class ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b require __DIR__ . '/platform_check.php'; spl_autoload_register(array('ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b', 'loadClassLoader'), true, true); - self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); spl_autoload_unregister(array('ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b', 'loadClassLoader')); $includePaths = require __DIR__ . '/include_paths.php'; $includePaths[] = get_include_path(); set_include_path(implode(PATH_SEPARATOR, $includePaths)); - require __DIR__ . '/autoload_static.php'; - call_user_func(\Composer\Autoload\ComposerStaticInit0018331147de7601e7552f7da8e3bb8b::getInitializer($loader)); + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit0018331147de7601e7552f7da8e3bb8b::getInitializer($loader)); + } else { + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } $loader->setClassMapAuthoritative(true); $loader->register(true); - $includeFiles = \Composer\Autoload\ComposerStaticInit0018331147de7601e7552f7da8e3bb8b::$files; + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInit0018331147de7601e7552f7da8e3bb8b::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } foreach ($includeFiles as $fileIdentifier => $file) { composerRequire0018331147de7601e7552f7da8e3bb8b($fileIdentifier, $file); } @@ -47,16 +60,11 @@ class ComposerAutoloaderInit0018331147de7601e7552f7da8e3bb8b } } -/** - * @param string $fileIdentifier - * @param string $file - * @return void - */ function composerRequire0018331147de7601e7552f7da8e3bb8b($fileIdentifier, $file) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { - $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; - require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; } } diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 8e76a331b..afed08926 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -8,16 +8,16 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b { public static $files = array ( '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', - '5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php', '023d27dca8066ef29e6739335ea73bad' => __DIR__ . '/..' . '/symfony/polyfill-php70/bootstrap.php', + '32dcc8afd4335739640db7d200c1971d' => __DIR__ . '/..' . '/symfony/polyfill-apcu/bootstrap.php', + '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', 'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', - '32dcc8afd4335739640db7d200c1971d' => __DIR__ . '/..' . '/symfony/polyfill-apcu/bootstrap.php', 'def43f6c87e4f8dfd0c9e1b1bab14fe8' => __DIR__ . '/..' . '/symfony/polyfill-iconv/bootstrap.php', - '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', '2c102faa651ef8ea5874edb585946bce' => __DIR__ . '/..' . '/swiftmailer/swiftmailer/lib/swift_required.php', ); @@ -420,6 +420,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'Combodo\\iTop\\Application\\TwigBase\\Twig\\TwigHelper' => __DIR__ . '/../..' . '/sources/application/TwigBase/Twig/TwigHelper.php', 'Combodo\\iTop\\Composer\\iTopComposer' => __DIR__ . '/../..' . '/sources/Composer/iTopComposer.php', 'Combodo\\iTop\\Controller\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.php', + 'Combodo\\iTop\\Controller\\OAuth\\OAuthWizardController' => __DIR__ . '/../..' . '/sources/Controller/OAuth/OAuthWizardController.php', 'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php', 'Combodo\\iTop\\DesignElement' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php', 'Combodo\\iTop\\TwigExtension' => __DIR__ . '/../..' . '/application/twigextension.class.inc.php', diff --git a/lib/composer/include_paths.php b/lib/composer/include_paths.php index af33c1491..d4fb96718 100644 --- a/lib/composer/include_paths.php +++ b/lib/composer/include_paths.php @@ -2,7 +2,7 @@ // include_paths.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/pages/oauth/ajax.wizard.php b/pages/oauth/ajax.wizard.php new file mode 100644 index 000000000..1d3ca1ce8 --- /dev/null +++ b/pages/oauth/ajax.wizard.php @@ -0,0 +1,63 @@ +SetOutputDataOnly(true); +$aResult = ['status' => 'success', 'data' => []]; +try { + $operation = utils::ReadParam('operation', ''); + + switch ($operation) { + case 'get_authorization_url': + $sProvider = utils::ReadParam('provider', '', false, 'raw'); + $sClientId = utils::ReadParam('client_id', '', false, 'raw'); + $sClientSecret = utils::ReadParam('client_secret', '', false, 'raw'); + $sScope = utils::ReadParam('scope', '', false, 'raw'); + $sAdditional = utils::ReadParam('additional', '', false, 'raw'); + $aAdditional = []; + parse_str($sAdditional, $aAdditional); + $sAuthorizationUrl = OAuthClientProviderFactory::getVendorProviderForAccessUrl($sProvider, $sClientId, $sClientSecret, $sScope, $aAdditional); + $aResult['data']['authorization_url'] = $sAuthorizationUrl; + break; + case 'get_display_authentication_results': + $sProvider = utils::ReadParam('provider', '', false, 'raw'); + $sRedirectUrl = utils::ReadParam('redirect_url', '', false, 'raw'); + $sClientId = utils::ReadParam('client_id', '', false, 'raw'); + $sClientSecret = utils::ReadParam('client_secret', '', false, 'raw'); + $sScope = utils::ReadParam('scope', '', false, 'raw'); + $sAdditional = utils::ReadParam('additional', '', false, 'raw'); + + $sRedirectUrlQuery = parse_url($sRedirectUrl)['query']; + $aOAuthResultDisplayClasses = utils::GetClassesForInterface('Combodo\iTop\Core\Authentication\Client\OAuth\IOAuthClientResultDisplay', '', array('[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]')); + $aAdditional = []; + parse_str($sAdditional, $aAdditional); + + $sProviderClass = "\Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProvider".$sProvider; + $sRedirectUrl = OAuthClientProviderAbstract::GetRedirectUri(); + + $aQuery = []; + parse_str($sRedirectUrlQuery, $aQuery); + $sCode = $aQuery['code']; + $oProvider = OAuthClientProviderFactory::getVendorProvider($sProvider, $sClientId, $sClientSecret, $sScope, $aAdditional); + $oAccessToken = OAuthClientProviderFactory::getAccessTokenFromCode($oProvider, $sCode); + + foreach($aOAuthResultDisplayClasses as $sOAuthClass) { + $aResult['data'][] = $sOAuthClass::GetResultDisplayScript($sClientId, $sClientSecret, $sProvider, $oAccessToken); + } + } +} +catch(Exception $e){ + $aResult['status'] = 'error'; + IssueLog::Error($e->getMessage()); +} +$oPage->SetData($aResult); +$oPage->output(); \ No newline at end of file diff --git a/pages/oauth/landing.php b/pages/oauth/landing.php new file mode 100644 index 000000000..d885ded25 --- /dev/null +++ b/pages/oauth/landing.php @@ -0,0 +1,26 @@ +AddCSSClass('ibo-oauth-wizard--side-pane'); +$oPage = new WebPage(Dict::S('UI:Schema:Title')); + +$sJS = <<add_script($sJS); + + +$oPage->output(); diff --git a/pages/oauth/wizard.php b/pages/oauth/wizard.php new file mode 100644 index 000000000..a88c90950 --- /dev/null +++ b/pages/oauth/wizard.php @@ -0,0 +1,159 @@ +AddCSSClass('ibo-oauth-wizard'); +$oPage = new iTopWebPage(Dict::S('UI:OAuth:Wizard:Page:Title')); +$oPage->SetContentLayout($oLayout); +$sReturnUri = utils::GetAbsoluteUrlAppRoot().'pages/oauth.landing.php'; +$sAjaxUri = utils::GetAbsoluteUrlAppRoot().'pages/ajax.oauth.wizard.php'; + +$sJS = << { + // remove any existing event listeners + + // window features + const strWindowFeatures = + 'toolbar=no, menubar=no, width=600, height=700, top=100, left=100'; + + if (windowObjectReference === null || windowObjectReference.closed) { + /* if the pointer to the window object in memory does not exist + or if such pointer exists but the window was closed */ + windowObjectReference = window.open(url, name, strWindowFeatures); + } else if (previousUrl !== url) { + /* if the resource to load is different, + then we load it in the already opened secondary window and then + we bring such window back on top/in front of its parent window. */ + windowObjectReference = window.open(url, name, strWindowFeatures); + windowObjectReference.focus(); + } else { + /* else the window reference must exist and the window + is not closed; therefore, we can bring it back on top of any other + window with the focus() method. There would be no need to re-create + the window or to reload the referenced resource. */ + windowObjectReference.focus(); + } + let oListener = window.setInterval(function(){ + windowObjectReference.postMessage('anyone', '$sReturnUri'); + }, 1000); + + + window.addEventListener("message", function (event){ + clearInterval(oListener); + $.post( + '$sAjaxUri', + { + operation: 'get_display_authentication_results', + provider: $('[name="provider"]:checked').val(), + client_id: $('[name="client_id"]').val(), + client_secret: $('[name="client_secret"]').val(), + scope: $(this).find('[name="scope"]').val(), + additional: $(this).find('[name="additional"]').val(), + redirect_url: event.data, + }, + function(oData){ + if(oData.status == 'success') + { + oData.data.forEach(function(item, index){ + new Function(item)(); + }); + } + $('.ibo-oauth-wizard--form--submit').trigger('leave_loading_state.button.itop'); + } + ); + }, false); + // add the listener for receiving a message from the popup + // assign the previous URL + previousUrl = url; + }; +JS; + +$oPage->add_script($sJS); +$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'/js/pages/backoffice/oauth.wizard.js'); + +$oOauthInputsPanel = PanelUIBlockFactory::MakeNeutral(Dict::S('UI:OAuth:Wizard:Form:Panel:Title')); +$oOauthInputsPanel->AddCSSClass('ibo-oauth-wizard'); + +$aOAuthClasses = utils::GetClassesForInterface('Combodo\iTop\Core\Authentication\Client\OAuth\IOAuthClientProvider', '', array('[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]')); +$sFormJs = <<
'); +$oOauthInputsPanel->AddSubBlock($oProviderSelect); +$sIsChecked = ' checked '; +foreach($aOAuthClasses as $sOAuthClass){ + $aColors = $sOAuthClass::GetVendorColors(); + $oProviderSelect->AddHtml(''); + $sIsChecked = ''; +} +$oForm = FormUIBlockFactory::MakeStandard(); +$oForm->AddCSSClasses(['ibo-oauth-wizard--form', 'ibo-oauth-wizard--form-'.strtolower($sOAuthClass::GetVendorName())]); +$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('url', '')); +foreach (['client_id' => Dict::S('UI:OAuth:Wizard:Form:Input:ClientId:Label'), + 'client_secret' => Dict::S('UI:OAuth:Wizard:Form:Input:ClientSecret:Label'), + 'scope' => Dict::S('UI:OAuth:Wizard:Form:Input:Scope:Label'), + 'additional' => Dict::S('UI:OAuth:Wizard:Form:Input:Additional:Label')] as $sName => $sLabel){ + $oForm->AddSubBlock(InputUIBlockFactory::MakeForInputWithLabel($sLabel, $sName, null, null, 'text')); +} +$oRedirectUriInput = InputUIBlockFactory::MakeForInputWithLabel(Dict::S('UI:OAuth:Wizard:Form:Input:RedirectUri:Label'), 'redirect_uri', OAuthClientProviderGoogle::GetRedirectUri(), null, 'text'); +$oRedirectUriInput->GetInput()->SetIsReadonly(true); +$oForm->AddSubBlock($oRedirectUriInput); + +$oForm->AddSubBlock(ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:OAuth:Wizard:Form:Button:Submit:Label'),'submit', '', true)->AddCSSClass('ibo-oauth-wizard--form--submit')); +$oForm->SetOnSubmitJsCode($sFormJs); +$oOauthInputsPanel->AddSubBlock($oForm); +$oProviderSelect->AddHtml('
'); +$oOauthInputsPanel->AddHtml(''); + +$sOnReadyJs = "$('#select_layout').controlgroup(); $('.ibo-oauth-wizard--result--panel .ibo-panel--collapsible-toggler').click();"; +$oPage->add_ready_script($sOnReadyJs); +$oOauthInputsPanel->AddHtml('
'.file_get_contents(APPROOT.'images/illustrations/undraw_access_account.svg').'
'); +$oPage->AddSubBlock($oOauthInputsPanel); + +$aOAuthResultDisplayClasses = utils::GetClassesForInterface('Combodo\iTop\Core\Authentication\Client\OAuth\IOAuthClientResultDisplay', '', array('[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]')); +foreach($aOAuthResultDisplayClasses as $sOAuthClass) { + $oPage->AddSubBlock($sOAuthClass::GetResultDisplayBlock()); +} + + +$oPage->output(); diff --git a/sources/Controller/OAuth/OAuthWizardController.php b/sources/Controller/OAuth/OAuthWizardController.php new file mode 100644 index 000000000..f109a2370 --- /dev/null +++ b/sources/Controller/OAuth/OAuthWizardController.php @@ -0,0 +1,48 @@ +AddLinkedScript(utils::GetAbsoluteUrlAppRoot().'/js/pages/backoffice/oauth.wizard.js'); + + $aOAuthClasses = [ + 'Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderAzure', + 'Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderGoogle', + ]; + foreach ($aOAuthClasses as $sOAuthClass) { + $aParams['aProviders'][] = [ + 'name' => $sOAuthClass::GetVendorName(), + 'icon' => $sOAuthClass::GetVendorIcon(), + 'colors' => $sOAuthClass::GetVendorColors(), + ]; + } + + $aParams['aInputs'] = [ + 'client_id' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:ClientId:Label'), 'read_only' => false, 'value' => ''], + 'client_secret' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:ClientSecret:Label'), 'read_only' => false, 'value' => ''], + 'scope' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:Scope:Label'), 'read_only' => false, 'value' => ''], + 'additional' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:Additional:Label'), 'read_only' => false, 'value' => ''], + 'redirect_uri' => ['type' => 'text', 'label' => Dict::S('UI:OAuth:Wizard:Form:Input:RedirectUri:Label'), 'read_only' => true, 'value' => OAuthClientProviderAbstract::GetRedirectUri()], + ]; + + $this->DisplayPage($aParams); + } +} \ No newline at end of file diff --git a/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php b/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php new file mode 100644 index 000000000..805686103 --- /dev/null +++ b/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php @@ -0,0 +1,5 @@ +oVendorProvider; + } + + /** + * @param \League\OAuth2\Client\Provider\GenericProvider $oVendorProvider + */ + public function SetVendorProvider(GenericProvider $oVendorProvider) + { + $this->oVendorProvider = $oVendorProvider; + } + + /** + * @return \League\OAuth2\Client\Token\AccessToken + */ + public function GetAccessToken(): AccessToken + { + return $this->oAccessToken; + } + + /** + * @param \League\OAuth2\Client\Token\AccessToken $oAccessToken + */ + public function SetAccessToken(AccessToken $oAccessToken) + { + $this->oAccessToken = $oAccessToken; + } + + /** + * @return string + */ + public static function GetVendorIcon(): string + { + return static::$sVendorIcon; + } + + /** + * @return string + */ + public static function GetVendorName(): string + { + return static::$sVendorName; + } + + + public static function getConfFromAccessToken($oAccessToken, $sClientId, $sClientSecret): string + { + $sAccessToken = $oAccessToken->getToken(); + $sRefreshToken = $oAccessToken->getRefreshToken(); + $sVendor = static::GetVendorName(); + + return << 'SMTP_OAuth', +'email_transport_smtp.oauth.provider' => '$sVendor', +'email_transport_smtp.oauth.client_id' => '$sClientId', +'email_transport_smtp.oauth.client_secret' => '$sClientSecret', +'email_transport_smtp.oauth.access_token' => '$sAccessToken', +'email_transport_smtp.oauth.refresh_token' => '$sRefreshToken', +EOF; + } + + /** + * @return array + */ + public static function GetVendorColors(): array + { + return static::$sVendorColors; + } + + /** + * @return void + * @throws \Exception + */ + public static function InitizalizeRedirectUri() + { + static::$sRedirectUri = utils::GetAbsoluteUrlAppRoot().'pages/oauth/landing.php'; + } + + /** + * @return string + */ + public static function GetRedirectUri(): string + { + if (static::$sRedirectUri === '') { + static::InitizalizeRedirectUri(); + } + + return static::$sRedirectUri; + } + + /** + * @return string + */ + public static function GetRequiredSMTPScope(): string + { + return static::$sRequiredSMTPScope; + } + + /** + * @return string + */ + public static function GetRequiredIMAPScope(): string + { + return static::$sRequiredIMAPScope; + } + + /** + * @return string + */ + public static function GetRequiredPOPScope(): string + { + return static::$sRequiredPOPScope; + } + + /** + * @return mixed + */ + public function GetScope() + { + return $this->sScope; + } + + /** + * @param mixed $sScope + */ + public function SetScope($sScope) + { + $this->sScope = $sScope; + } + +} \ No newline at end of file diff --git a/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php b/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php new file mode 100644 index 000000000..cbf298377 --- /dev/null +++ b/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php @@ -0,0 +1,46 @@ +oVendorProvider = new Azure(array_merge([ + 'prompt' => 'consent', + 'scope' => 'offline_access', + 'defaultEndPointVersion' => Azure::ENDPOINT_VERSION_2_0, + ], + $aVendorProvider), $collaborators); + + if (!empty($aAccessTokenParams)) { + $this->oAccessToken = new AccessToken([ + "access_token" => $aAccessTokenParams["access_token"], + "expires_in" => -1, + "refresh_token" => $aAccessTokenParams["refresh_token"], + "token_type" => "Bearer", + ]); + } + + if (isset($aVendorProvider['scope'])) { + $this->SetScope($aVendorProvider['scope']); + } + } +} \ No newline at end of file diff --git a/sources/Core/Authentication/Client/OAuth/OAuthClientProviderFactory.php b/sources/Core/Authentication/Client/OAuth/OAuthClientProviderFactory.php new file mode 100644 index 000000000..8ede0f6fe --- /dev/null +++ b/sources/Core/Authentication/Client/OAuth/OAuthClientProviderFactory.php @@ -0,0 +1,73 @@ +Get('email_transport_smtp.oauth.provider'); // email_transport_smtp.oauth.provider + $sProviderClass = "\Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProvider".$sProviderVendor; + $aProviderVendorParams = [ + 'clientId' => MetaModel::GetConfig()->Get('email_transport_smtp.oauth.client_id'), // email_transport_smtp.oauth.client_id + 'clientSecret' => MetaModel::GetConfig()->Get('email_transport_smtp.oauth.client_secret'),// email_transport_smtp.oauth.client_secret + 'redirectUri' => $sProviderClass::GetRedirectUri(), + 'scope' => $sProviderClass::GetRequiredSMTPScope() + ]; + $aAccessTokenParams = [ + "access_token" => MetaModel::GetConfig()->Get('email_transport_smtp.oauth.access_token'), // email_transport_smtp.oauth.access_token + "refresh_token" => MetaModel::GetConfig()->Get('email_transport_smtp.oauth.refresh_token'), // email_transport_smtp.oauth.refresh_token + 'scope' => $sProviderClass::GetRequiredSMTPScope() + ]; + $aCollaborators = [ + 'httpClient' => new Client(['verify' => false]), + ]; + + return new $sProviderClass($aProviderVendorParams, $aCollaborators, $aAccessTokenParams); + } + public static function getVendorProvider($sProviderVendor, $sClientId, $sClientSecret, $sScope, $aAdditional){ + $sRedirectUrl = OAuthClientProviderAbstract::GetRedirectUri(); + $sProviderClass = "\Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProvider".$sProviderVendor; + $aCollaborators = [ + 'httpClient' => new Client(['verify' => false]), + ]; + + return new $sProviderClass(array_merge(['clientId' => $sClientId, 'clientSecret' => $sClientSecret, 'redirectUri' => $sRedirectUrl, 'scope' => $sScope], $aAdditional), $aCollaborators); + } + + public static function getVendorProviderForAccessUrl($sProviderVendor, $sClientId, $sClientSecret, $sScope, $aAdditional){ + $oProvider = static::getVendorProvider($sProviderVendor, $sClientId, $sClientSecret, $sScope, $aAdditional); + return $oProvider->GetVendorProvider()->getAuthorizationUrl([ + 'scope' => [ + $sScope + ], + ]); + } + + /** + * @param \Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderAbstract $oProvider + * @param $sCode + * + * @return AccessTokenInterface + * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException + */ + public static function getAccessTokenFromCode($oProvider, $sCode) + { + return $oProvider->GetVendorProvider()->getAccessToken('authorization_code', ['code' => $sCode, 'scope' => $oProvider->GetScope()]); + } + + public static function getConfFromRedirectUrl($sProviderVendor, $sClientId, $sClientSecret, $sRedirectUrlQuery) + { + $sRedirectUrl = OAuthClientProviderAbstract::GetRedirectUri(); + $sProviderClass = "\Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProvider".$sProviderVendor; + $aQuery = []; + parse_str($sRedirectUrlQuery, $aQuery); + $sCode = $aQuery['code']; + $oProvider = new $sProviderClass(['clientId' => $sClientId, 'clientSecret' => $sClientSecret, 'redirectUri' => $sRedirectUrl]); + return $sProviderClass::getConfFromAccessToken($oProvider->GetVendorProvider()->getAccessToken('authorization_code', ['code' => $sCode]), $sClientId, $sClientSecret); + } + +} \ No newline at end of file diff --git a/sources/Core/Authentication/Client/OAuth/OAuthClientProviderGoogle.php b/sources/Core/Authentication/Client/OAuth/OAuthClientProviderGoogle.php new file mode 100644 index 000000000..bd2a1e3b6 --- /dev/null +++ b/sources/Core/Authentication/Client/OAuth/OAuthClientProviderGoogle.php @@ -0,0 +1,41 @@ +oVendorProvider = new Google(array_merge(['prompt' => 'consent', 'accessType' => 'offline'], $aVendorProvider), $collaborators); + + if (!empty($aAccessTokenParams)) { + $this->oAccessToken = new AccessToken([ + "access_token" => $aAccessTokenParams["access_token"], + "expires_in" => -1, + "refresh_token" => $aAccessTokenParams["refresh_token"], + "token_type" => "Bearer", + ]); + } + + if (isset($aVendorProvider['scope'])) { + $this->SetScope($aVendorProvider['scope']); + } + } +} \ No newline at end of file diff --git a/sources/Core/Authentication/Client/OAuth/OAuthClientResultDisplayConf.php b/sources/Core/Authentication/Client/OAuth/OAuthClientResultDisplayConf.php new file mode 100644 index 000000000..92c1bdaa4 --- /dev/null +++ b/sources/Core/Authentication/Client/OAuth/OAuthClientResultDisplayConf.php @@ -0,0 +1,40 @@ +AddCSSClass('ibo-oauth-wizard--result--panel'); + $oConfResultPanel->SetIsCollapsible(true); + $oConfResultPanel->AddHtml('

'.Dict::S('UI:OAuthEmailSynchro:Wizard:ResultConf:Panel:Description').'

'); + $oConfResultPanel->AddHtml('
'); + return $oConfResultPanel; + } + + public static function GetResultDisplayScript($sClientId, $sClientSecret, $sVendor, AccessToken $oAccessToken) + { + $sAccessToken = $oAccessToken->getToken(); + $sRefreshToken = $oAccessToken->getRefreshToken(); + $sConf = << 'SMTP_OAuth', +'email_transport_smtp.oauth.provider' => '$sVendor', +'email_transport_smtp.oauth.client_id' => '$sClientId', +'email_transport_smtp.oauth.client_secret' => '$sClientSecret', +'email_transport_smtp.oauth.access_token' => '$sAccessToken', +'email_transport_smtp.oauth.refresh_token' => '$sRefreshToken', +EOF; + $sConf = json_encode($sConf); + + return <<setUsername($config['username']); + } + } + + // Call parent with original arguments + parent::__construct($host, $port, $origConfig); + } + + /** + * @param OAuthClientProviderAbstract $oProvider + * + * @return void + */ + public static function setProvider(OAuthClientProviderAbstract $oProvider): void + { + self::$oProvider = $oProvider; + } + + /** + * Perform LOGIN authentication with supplied credentials + * + */ + public function auth() + { + try { + if (empty(self::$oProvider->GetAccessToken())) { + throw new IdentityProviderException('Not prior authentication to OAuth', 255, []); + } elseif (self::$oProvider->GetAccessToken()->hasExpired()) { + self::$oProvider->SetAccessToken(self::$oProvider->GetVendorProvider()->getAccessToken('refresh_token', [ + 'refresh_token' => self::$oProvider->GetAccessToken()->getRefreshToken(), + 'scope' => self::$oProvider->GetScope(), + ])); + } + } + catch (IdentityProviderException $e) { + IssueLog::Error('Failed to get oAuth credentials for outgoing mails for provider '.self::$oProvider::GetVendorName(), static::LOG_CHANNEL); + + return false; + } + $sAccessToken = self::$oProvider->GetAccessToken()->getToken(); + + if (empty($sAccessToken)) { + IssueLog::Error('No OAuth token for outgoing mails for provider '.self::$oProvider::GetVendorName(), static::LOG_CHANNEL); + + return false; + } + + $this->_send('AUTH XOAUTH2 '.base64_encode("user=$this->username\1auth=Bearer $sAccessToken\1\1")); + IssueLog::Debug("SMTP Oauth sending AUTH XOAUTH2 user=$this->username auth=Bearer $sAccessToken", static::LOG_CHANNEL); + + try { + while (true) { + $sResponse = $this->_receive(60); + + IssueLog::Debug("SMTP Oauth receiving $sResponse", static::LOG_CHANNEL); + + if ($sResponse === '+') { + // Send empty client response. + $this->_send(''); + } else { + if (preg_match('/Unauthorized/i', $sResponse) || + preg_match('/Rejected/i', $sResponse) || + preg_match('/^(535|432|454|534|500|530|538)/', $sResponse)) { + IssueLog::Error('Unable to authenticate for outgoing mails for provider '.self::$oProvider::GetVendorName()." Error: $sResponse", static::LOG_CHANNEL); + + return false; + } + if (preg_match("/OK /i", $sResponse) || + preg_match('/Accepted/i', $sResponse) || + preg_match('/^235/i', $sResponse)) { + $this->auth = true; + + return true; + } + + } + } + } catch (RuntimeException $e) { + IssueLog::Error('Timeout connection for outgoing mails for provider '.self::$oProvider::GetVendorName(), static::LOG_CHANNEL); + } + return false; + } +} diff --git a/sources/Core/Email/EmailFactory.php b/sources/Core/Email/EmailFactory.php new file mode 100644 index 000000000..56d84f21b --- /dev/null +++ b/sources/Core/Email/EmailFactory.php @@ -0,0 +1,6 @@ + + + +/** + * Send an email (abstraction for synchronous/asynchronous modes) + * + * @copyright Copyright (C) 2010-2021 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ +@include APPROOT."/core/oauth.php"; + +use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderFactory; +use Laminas\Mail\Header\ContentType; +use Laminas\Mail\Message; +use Laminas\Mail\Transport\File; +use Laminas\Mail\Transport\FileOptions; +use Laminas\Mail\Transport\Smtp; +use Laminas\Mail\Transport\SmtpOptions; +use Laminas\Mime\Mime; +use Laminas\Mime\Part; +use Pelago\Emogrifier\CssInliner; +use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter; +use Pelago\Emogrifier\HtmlProcessor\HtmlPruner; + +define ('EMAIL_SEND_OK', 0); +define ('EMAIL_SEND_PENDING', 1); +define ('EMAIL_SEND_ERROR', 2); + +class EMail +{ + // Serialization formats + const ORIGINAL_FORMAT = 1; // Original format, consisting in serializing the whole object, inculding the Swift Mailer's object. + // Did not work with attachements since their binary representation cannot be stored as a valid UTF-8 string + const FORMAT_V2 = 2; // New format, only the raw data are serialized (base64 encoded if needed) + + protected static $m_oConfig = null; + protected $m_aData; // For storing data to serialize + + public function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE) + { + if (is_null(self::$m_oConfig)) + { + self::$m_oConfig = new Config($sConfigFile); + } + } + + protected $m_oMessage; + + public function __construct() + { + $this->m_aData = array(); + $this->m_oMessage = new Message(); + $this->m_oMessage->setEncoding('UTF-8'); + $this->SetRecipientFrom(MetaModel::GetConfig()->Get('email_default_sender_address'), MetaModel::GetConfig()->Get('email_default_sender_label')); + } + + /** + * Custom serialization method + * No longer use the brute force "serialize" method since + * 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field) + * 2) The size tends to be quite big (sometimes ten times the size of the email) + */ + public function SerializeV2() + { + return serialize($this->m_aData); + } + + /** + * Custom de-serialization method + * + * @param string $sSerializedMessage The serialized representation of the message + * + * @return \EMail + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \Symfony\Component\CssSelector\Exception\SyntaxErrorException + */ + public static function UnSerializeV2($sSerializedMessage) + { + $aData = unserialize($sSerializedMessage); + $oMessage = new Email(); + + if (array_key_exists('body', $aData)) + { + $oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']); + } + if (array_key_exists('message_id', $aData)) + { + $oMessage->SetMessageId($aData['message_id']); + } + if (array_key_exists('bcc', $aData)) + { + $oMessage->SetRecipientBCC($aData['bcc']); + } + if (array_key_exists('cc', $aData)) + { + $oMessage->SetRecipientCC($aData['cc']); + } + if (array_key_exists('from', $aData)) + { + $oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']); + } + if (array_key_exists('reply_to', $aData)) + { + $oMessage->SetRecipientReplyTo($aData['reply_to']['address'], $aData['reply_to']['label']); + } + if (array_key_exists('to', $aData)) + { + $oMessage->SetRecipientTO($aData['to']); + } + if (array_key_exists('subject', $aData)) + { + $oMessage->SetSubject($aData['subject']); + } + + if (array_key_exists('headers', $aData)) + { + foreach($aData['headers'] as $sKey => $sValue) + { + $oMessage->AddToHeader($sKey, $sValue); + } + } + if (array_key_exists('parts', $aData)) + { + foreach($aData['parts'] as $aPart) + { + $oMessage->AddPart($aPart['text'], $aPart['mimeType']); + } + } + if (array_key_exists('attachments', $aData)) + { + foreach($aData['attachments'] as $aAttachment) + { + $oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']); + } + } + return $oMessage; + } + + protected function SendAsynchronous(&$aIssues, $oLog = null) + { + try + { + AsyncSendEmail::AddToQueue($this, $oLog); + } + catch(Exception $e) + { + $aIssues = array($e->GetMessage()); + return EMAIL_SEND_ERROR; + } + $aIssues = array(); + return EMAIL_SEND_PENDING; + } + + /** + * @throws \Exception + */ + protected function SendSynchronous(&$aIssues, $oLog = null) + { + + $this->LoadConfig(); + + $sTransport = self::$m_oConfig->Get('email_transport'); + switch ($sTransport) + { + case 'SMTP': + $sHost = self::$m_oConfig->Get('email_transport_smtp.host'); + $sPort = self::$m_oConfig->Get('email_transport_smtp.port'); + $sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption'); + $sUserName = self::$m_oConfig->Get('email_transport_smtp.username'); + $sPassword = self::$m_oConfig->Get('email_transport_smtp.password'); + + $oTransport = new Smtp(); + $aOptions= [ + 'host' => $sHost, + 'port' => $sPort, + 'connection_class' => 'login', + 'connection_config' => [ + 'ssl' => $sEncryption, + ], + ]; + if (strlen($sUserName) > 0) + { + $aOptions['connection_config']['username'] = $sUserName; + $aOptions['connection_config']['password'] = $sPassword; + } + $oOptions = new SmtpOptions($aOptions); + $oTransport->setOptions($oOptions); + break; + case 'SMTP_OAuth': + $sHost = self::$m_oConfig->Get('email_transport_smtp.host'); + $sPort = self::$m_oConfig->Get('email_transport_smtp.port'); + $sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption'); + $sUserName = self::$m_oConfig->Get('email_transport_smtp.username'); + + $oTransport = new Smtp(); + $aOptions= [ + 'host' => $sHost, + 'port' => $sPort, + 'connection_class' => 'Laminas\Mail\Protocol\Smtp\Auth\Oauth', + 'connection_config' => [ + 'ssl' => $sEncryption, + ], + ]; + if (strlen($sUserName) > 0) + { + $aOptions['connection_config']['username'] = $sUserName; + } + $oOptions = new SmtpOptions($aOptions); + $oTransport->setOptions($oOptions); + + \Laminas\Mail\Protocol\Smtp\Auth\Oauth::setProvider(OAuthClientProviderFactory::getProviderForSMTP()); + break; + case 'Null': + $oTransport = new Smtp(); + break; + + case 'LogFile': + $oTransport = new File(); + $aOptions = new FileOptions([ + 'path' => APPROOT.'log/mail.log', + ]); + $oTransport->setOptions($aOptions); + break; + + case 'PHPMail': + default: + $oTransport = new Smtp(); + } + + $oKPI = new ExecutionKPI(); + try + { + $oTransport->send($this->m_oMessage); + $aIssues = array(); + $oKPI->ComputeStats('Email Sent', 'Succeded'); + return EMAIL_SEND_OK; + } + catch(Laminas\Mail\Transport\Exception\RuntimeException $e){ + IssueLog::Warning('Email sending failed: Some recipients were invalid'); + $aIssues = array('Some recipients were invalid.'); + $oKPI->ComputeStats('Email Sent', 'Error received'); + return EMAIL_SEND_ERROR; + } + catch (Exception $e) + { + $oKPI->ComputeStats('Email Sent', 'Error received'); + throw $e; + } + } + + /** + * Reprocess the body of the message (if it is an HTML message) + * to replace the URL of images based on attachments by a link + * to an embedded image (i.e. cid:....) and returns images to be attached as an array + * + * @param string $sBody Email body to process/alter + * + * @return array Array of Part that needs to be added as inline attachment later to render as embed + * @throws \ArchivedObjectException + * @throws \CoreException + */ + protected function EmbedInlineImages(string &$sBody) + { + $oDOMDoc = new DOMDocument(); + $oDOMDoc->preserveWhitespace = true; + @$oDOMDoc->loadHTML(''.$sBody); // For loading HTML chunks where the character set is not specified + + $oXPath = new DOMXPath($oDOMDoc); + $sXPath = '//img[@'.InlineImage::DOM_ATTR_ID.']'; + $oImagesList = $oXPath->query($sXPath); + $oImagesContent = new \Laminas\Mime\Message(); + $aImagesParts = []; + if ($oImagesList->length != 0) + { + foreach($oImagesList as $oImg) + { + $iAttId = $oImg->getAttribute(InlineImage::DOM_ATTR_ID); + $oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */); + if ($oAttachment) + { + $sImageSecret = $oImg->getAttribute('data-img-secret'); + $sAttachmentSecret = $oAttachment->Get('secret'); + if ($sImageSecret !== $sAttachmentSecret) + { + // @see N°1921 + // If copying from another iTop we could get an IMG pointing to an InlineImage with wrong secret + continue; + } + + $oDoc = $oAttachment->Get('contents'); + + $sCid = uniqid('', true); + + $oNewAttachment = new Part($oDoc->GetData()); + $oNewAttachment->id = $sCid; + $oNewAttachment->type = $oDoc->GetMimeType(); + $oNewAttachment->filename = $oDoc->GetFileName(); + $oNewAttachment->disposition = Mime::DISPOSITION_INLINE; + $oNewAttachment->encoding = Mime::ENCODING_BASE64; + + $oImagesContent->addPart($oNewAttachment); + $oImg->setAttribute('src', 'cid:'.$sCid); + $aImagesParts[] = $oNewAttachment; + } + } + } + $sBody = $oDOMDoc->saveHTML(); + return $aImagesParts; + } + + public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null) + { + //select a default sender if none is provided. + if(empty($this->m_aData['from']['address']) && !empty($this->m_aData['to'])){ + $this->SetRecipientFrom($this->m_aData['to']); + } + + if ($bForceSynchronous) + { + return $this->SendSynchronous($aIssues, $oLog); + } + else + { + $bConfigASYNC = MetaModel::GetConfig()->Get('email_asynchronous'); + if ($bConfigASYNC) + { + return $this->SendAsynchronous($aIssues, $oLog); + } + else + { + return $this->SendSynchronous($aIssues, $oLog); + } + } + } + + public function AddToHeader($sKey, $sValue) + { + if (!array_key_exists('headers', $this->m_aData)) + { + $this->m_aData['headers'] = array(); + } + $this->m_aData['headers'][$sKey] = $sValue; + + if (strlen($sValue) > 0) + { + $oHeaders = $this->m_oMessage->getHeaders(); + switch(strtolower($sKey)) + { + case 'return-path': + $this->m_oMessage->setReturnPath($sValue); + break; + + default: + $oHeaders->addHeaderLine($sKey, $sValue); + } + } + } + + public function SetMessageId($sId) + { + $this->m_aData['message_id'] = $sId; + + // Note: Swift will add the angle brackets for you + // so let's remove the angle brackets if present, for historical reasons + $sId = str_replace(array('<', '>'), '', $sId); + + $this->m_oMessage->getHeaders()->addHeaderLine('Message-ID', $sId); + } + + public function SetReferences($sReferences) + { + $this->AddToHeader('References', $sReferences); + } + + /** + * Set the "In-Reply-To" header to allow emails to group as a conversation in modern mail clients (GMail, Outlook 2016+, ...) + * + * @link https://en.wikipedia.org/wiki/Email#Header_fields + * + * @param string $sMessageId + * + * @since 3.0.1 N°4849 + */ + public function SetInReplyTo(string $sMessageId) + { + $this->AddToHeader('In-Reply-To', $sMessageId); + } + + /** + * Set current Email body and process inline images. + * + * @param $sBody + * @param string $sMimeType + * @param $sCustomStyles + * + * @return void + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \Symfony\Component\CssSelector\Exception\SyntaxErrorException + */ + public function SetBody($sBody, string $sMimeType = Mime::TYPE_HTML, $sCustomStyles = null) + { + $oBody = new Laminas\Mime\Message(); + $aAdditionalParts = []; + + if (($sMimeType === Mime::TYPE_HTML) && ($sCustomStyles !== null)) { + $oDomDocument = CssInliner::fromHtml($sBody)->inlineCss($sCustomStyles)->getDomDocument(); + HtmlPruner::fromDomDocument($oDomDocument)->removeElementsWithDisplayNone(); + $sBody = CssToAttributeConverter::fromDomDocument($oDomDocument)->convertCssToVisualAttributes()->render(); // Adds html/body tags if not already present + } + $this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType); + + // We don't want these modifications in m_aData['body'], otherwise it'll ruin asynchronous mail as they go through this method twice + if ($sMimeType === Mime::TYPE_HTML){ + $aAdditionalParts = $this->EmbedInlineImages($sBody); + } + + // Add body content to as a new part + $oNewPart = new Part($sBody); + $oNewPart->encoding = Mime::ENCODING_8BIT; + $oNewPart->type = $sMimeType; + $oBody->addPart($oNewPart); + + // Add additional images as new body parts + foreach ($aAdditionalParts as $oAdditionalPart) { + $oBody->addPart($oAdditionalPart); + } + + if($oBody->isMultiPart()){ + $oContentTypeHeader = $this->m_oMessage->getHeaders(); + foreach ($oContentTypeHeader as $oHeader) { + if (!$oHeader instanceof ContentType) { + continue; + } + + $oHeader->setType(Mime::MULTIPART_MIXED); + $oHeader->addParameter('boundary', $oBody->getMime()->boundary()); + break; + } + } + + $this->m_oMessage->setBody($oBody); + } + + /** + * Add a new part to the existing body + * @param $sText + * @param string $sMimeType + * + * @return void + */ + public function AddPart($sText, string $sMimeType = Mime::TYPE_HTML) + { + if (!array_key_exists('parts', $this->m_aData)) + { + $this->m_aData['parts'] = array(); + } + $this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType); + $oNewPart = new Part($sText); + $oNewPart->encoding = Mime::ENCODING_8BIT; + $oNewPart->type = $sMimeType; + $this->m_oMessage->getBody()->addPart($oNewPart); + } + + public function AddAttachment($data, $sFileName, $sMimeType) + { + $oBody = $this->m_oMessage->getBody(); + + if(!$oBody->isMultiPart()){ + $multipart_content = new Part($oBody->generateMessage()); + $multipart_content->setType($oBody->getParts()[0]->getType()); + $multipart_content->setBoundary($oBody->getMime()->boundary()); + + $oBody = new Laminas\Mime\Message(); + $oBody->addPart($multipart_content); + } + + if (!array_key_exists('attachments', $this->m_aData)) + { + $this->m_aData['attachments'] = array(); + } + $this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType); + $oNewAttachment = new Part($data); + $oNewAttachment->type = $sMimeType; + $oNewAttachment->filename = $sFileName; + $oNewAttachment->disposition = Mime::DISPOSITION_ATTACHMENT; + $oNewAttachment->encoding = Mime::ENCODING_BASE64; + + + $oBody->addPart($oNewAttachment); + + if($oBody->isMultiPart()){ + $oContentTypeHeader = $this->m_oMessage->getHeaders(); + foreach ($oContentTypeHeader as $oHeader) { + if (!$oHeader instanceof ContentType) { + continue; + } + + $oHeader->setType(Mime::MULTIPART_MIXED); + $oHeader->addParameter('boundary', $oBody->getMime()->boundary()); + break; + } + } + + $this->m_oMessage->setBody($oBody); + } + + public function SetSubject($sSubject) + { + $this->m_aData['subject'] = $sSubject; + $this->m_oMessage->setSubject($sSubject); + } + + public function GetSubject() + { + return $this->m_oMessage->getSubject(); + } + + /** + * Helper to transform and sanitize addresses + * - get rid of empty addresses + */ + protected function AddressStringToArray($sAddressCSVList) + { + $aAddresses = array(); + foreach(explode(',', $sAddressCSVList) as $sAddress) + { + $sAddress = trim($sAddress); + if (strlen($sAddress) > 0) + { + $aAddresses[] = $sAddress; + } + } + return $aAddresses; + } + + public function SetRecipientTO($sAddress) + { + $this->m_aData['to'] = $sAddress; + if (!empty($sAddress)) + { + $aAddresses = $this->AddressStringToArray($sAddress); + $this->m_oMessage->setTo($aAddresses); + } + } + + public function GetRecipientTO($bAsString = false) + { + $aRes = $this->m_oMessage->getTo(); + if ($aRes === null || $aRes->count() === 0) + { + // There is no "To" header field + $aRes = array(); + } + if ($bAsString) + { + $aStrings = array(); + foreach ($aRes as $oEmail) + { + $sName = $oEmail->getName(); + $sEmail = $oEmail->getEmail(); + if (is_null($sName)) + { + $aStrings[] = $sEmail; + } + else + { + $sName = str_replace(array('<', '>'), '', $sName); + $aStrings[] = "$sName <$sEmail>"; + } + } + return implode(', ', $aStrings); + } + else + { + return $aRes; + } + } + + public function SetRecipientCC($sAddress) + { + $this->m_aData['cc'] = $sAddress; + if (!empty($sAddress)) + { + $aAddresses = $this->AddressStringToArray($sAddress); + $this->m_oMessage->setCc($aAddresses); + } + } + + public function SetRecipientBCC($sAddress) + { + $this->m_aData['bcc'] = $sAddress; + if (!empty($sAddress)) + { + $aAddresses = $this->AddressStringToArray($sAddress); + $this->m_oMessage->setBcc($aAddresses); + } + } + + public function SetRecipientFrom($sAddress, $sLabel = '') + { + $this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel); + if ($sLabel != '') + { + $this->m_oMessage->setFrom(array($sAddress => $sLabel)); + } + else if (!empty($sAddress)) + { + $this->m_oMessage->setFrom($sAddress); + } + } + + public function SetRecipientReplyTo($sAddress, $sLabel = '') + { + $this->m_aData['reply_to'] = array('address' => $sAddress, 'label' => $sLabel); + if ($sLabel != '') + { + $this->m_oMessage->setReplyTo(array($sAddress => $sLabel)); + } + else if (!empty($sAddress)) + { + $this->m_oMessage->setReplyTo($sAddress); + } + } + +} diff --git a/sources/Core/Email/EmailSwiftMailer.php b/sources/Core/Email/EmailSwiftMailer.php new file mode 100644 index 000000000..b0454a049 --- /dev/null +++ b/sources/Core/Email/EmailSwiftMailer.php @@ -0,0 +1,550 @@ + + + +/** + * Send an email (abstraction for synchronous/asynchronous modes) + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +use Pelago\Emogrifier\CssInliner; +use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter; +use Pelago\Emogrifier\HtmlProcessor\HtmlPruner; + +Swift_Preferences::getInstance()->setCharset('UTF-8'); + + +define ('EMAIL_SEND_OK', 0); +define ('EMAIL_SEND_PENDING', 1); +define ('EMAIL_SEND_ERROR', 2); + +class EmailSwiftMailer +{ + // Serialization formats + const ORIGINAL_FORMAT = 1; // Original format, consisting in serializing the whole object, inculding the Swift Mailer's object. + // Did not work with attachements since their binary representation cannot be stored as a valid UTF-8 string + const FORMAT_V2 = 2; // New format, only the raw data are serialized (base64 encoded if needed) + + protected static $m_oConfig = null; + protected $m_aData; // For storing data to serialize + + public function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE) + { + if (is_null(self::$m_oConfig)) + { + self::$m_oConfig = new Config($sConfigFile); + } + } + + protected $m_oMessage; + + public function __construct() + { + $this->m_aData = array(); + $this->m_oMessage = new Swift_Message(); + $this->SetRecipientFrom(MetaModel::GetConfig()->Get('email_default_sender_address'), MetaModel::GetConfig()->Get('email_default_sender_label')); + } + + /** + * Custom serialization method + * No longer use the brute force "serialize" method since + * 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field) + * 2) The size tends to be quite big (sometimes ten times the size of the email) + */ + public function SerializeV2() + { + return serialize($this->m_aData); + } + + /** + * Custom de-serialization method + * @param string $sSerializedMessage The serialized representation of the message + */ + static public function UnSerializeV2($sSerializedMessage) + { + $aData = unserialize($sSerializedMessage); + $oMessage = new Email(); + + if (array_key_exists('body', $aData)) + { + $oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']); + } + if (array_key_exists('message_id', $aData)) + { + $oMessage->SetMessageId($aData['message_id']); + } + if (array_key_exists('bcc', $aData)) + { + $oMessage->SetRecipientBCC($aData['bcc']); + } + if (array_key_exists('cc', $aData)) + { + $oMessage->SetRecipientCC($aData['cc']); + } + if (array_key_exists('from', $aData)) + { + $oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']); + } + if (array_key_exists('reply_to', $aData)) + { + $oMessage->SetRecipientReplyTo($aData['reply_to']); + } + if (array_key_exists('to', $aData)) + { + $oMessage->SetRecipientTO($aData['to']); + } + if (array_key_exists('subject', $aData)) + { + $oMessage->SetSubject($aData['subject']); + } + + + if (array_key_exists('headers', $aData)) + { + foreach($aData['headers'] as $sKey => $sValue) + { + $oMessage->AddToHeader($sKey, $sValue); + } + } + if (array_key_exists('parts', $aData)) + { + foreach($aData['parts'] as $aPart) + { + $oMessage->AddPart($aPart['text'], $aPart['mimeType']); + } + } + if (array_key_exists('attachments', $aData)) + { + foreach($aData['attachments'] as $aAttachment) + { + $oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']); + } + } + return $oMessage; + } + + protected function SendAsynchronous(&$aIssues, $oLog = null) + { + try + { + AsyncSendEmail::AddToQueue($this, $oLog); + } + catch(Exception $e) + { + $aIssues = array($e->GetMessage()); + return EMAIL_SEND_ERROR; + } + $aIssues = array(); + return EMAIL_SEND_PENDING; + } + + protected function SendSynchronous(&$aIssues, $oLog = null) + { + // If the body of the message is in HTML, embed all images based on attachments + $this->EmbedInlineImages(); + + $this->LoadConfig(); + + $sTransport = self::$m_oConfig->Get('email_transport'); + switch ($sTransport) + { + case 'SMTP': + $sHost = self::$m_oConfig->Get('email_transport_smtp.host'); + $sPort = self::$m_oConfig->Get('email_transport_smtp.port'); + $sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption'); + $sUserName = self::$m_oConfig->Get('email_transport_smtp.username'); + $sPassword = self::$m_oConfig->Get('email_transport_smtp.password'); + + $oTransport = new Swift_SmtpTransport($sHost, $sPort, $sEncryption); + if (strlen($sUserName) > 0) + { + $oTransport->setUsername($sUserName); + $oTransport->setPassword($sPassword); + } + break; + + case 'Null': + $oTransport = new Swift_NullTransport(); + break; + + case 'LogFile': + $oTransport = new Swift_LogFileTransport(); + $oTransport->setLogFile(APPROOT.'log/mail.log'); + break; + + case 'PHPMail': + default: + $oTransport = new Swift_SendmailTransport(); + } + + $oMailer = new Swift_Mailer($oTransport); + + $aFailedRecipients = array(); + $this->m_oMessage->setMaxLineLength(0); + $oKPI = new ExecutionKPI(); + try + { + $iSent = $oMailer->send($this->m_oMessage, $aFailedRecipients); + if ($iSent === 0) + { + // Beware: it seems that $aFailedRecipients sometimes contains the recipients that actually received the message !!! + IssueLog::Warning('Email sending failed: Some recipients were invalid, aFailedRecipients contains: '.implode(', ', $aFailedRecipients)); + $aIssues = array('Some recipients were invalid.'); + $oKPI->ComputeStats('Email Sent', 'Error received'); + return EMAIL_SEND_ERROR; + } + else + { + $aIssues = array(); + $oKPI->ComputeStats('Email Sent', 'Succeded'); + return EMAIL_SEND_OK; + } + } + catch (Exception $e) + { + $oKPI->ComputeStats('Email Sent', 'Error received'); + throw $e; + } + } + + /** + * Reprocess the body of the message (if it is an HTML message) + * to replace the URL of images based on attachments by a link + * to an embedded image (i.e. cid:....) + */ + protected function EmbedInlineImages() + { + if ($this->m_aData['body']['mimeType'] == 'text/html') + { + $oDOMDoc = new DOMDocument(); + $oDOMDoc->preserveWhitespace = true; + @$oDOMDoc->loadHTML(''.$this->m_aData['body']['body']); // For loading HTML chunks where the character set is not specified + + $oXPath = new DOMXPath($oDOMDoc); + $sXPath = '//img[@'.InlineImage::DOM_ATTR_ID.']'; + $oImagesList = $oXPath->query($sXPath); + + if ($oImagesList->length != 0) + { + foreach($oImagesList as $oImg) + { + $iAttId = $oImg->getAttribute(InlineImage::DOM_ATTR_ID); + $oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */); + if ($oAttachment) + { + $sImageSecret = $oImg->getAttribute('data-img-secret'); + $sAttachmentSecret = $oAttachment->Get('secret'); + if ($sImageSecret !== $sAttachmentSecret) + { + // @see N°1921 + // If copying from another iTop we could get an IMG pointing to an InlineImage with wrong secret + continue; + } + + $oDoc = $oAttachment->Get('contents'); + $oSwiftImage = new Swift_Image($oDoc->GetData(), $oDoc->GetFileName(), $oDoc->GetMimeType()); + $sCid = $this->m_oMessage->embed($oSwiftImage); + $oImg->setAttribute('src', $sCid); + } + } + } + $sHtmlBody = $oDOMDoc->saveHTML(); + $this->m_oMessage->setBody($sHtmlBody, 'text/html', 'UTF-8'); + } + } + + public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null) + { + //select a default sender if none is provided. + if(empty($this->m_aData['from']['address']) && !empty($this->m_aData['to'])){ + $this->SetRecipientFrom($this->m_aData['to']); + } + + if ($bForceSynchronous) + { + return $this->SendSynchronous($aIssues, $oLog); + } + else + { + $bConfigASYNC = MetaModel::GetConfig()->Get('email_asynchronous'); + if ($bConfigASYNC) + { + return $this->SendAsynchronous($aIssues, $oLog); + } + else + { + return $this->SendSynchronous($aIssues, $oLog); + } + } + } + + public function AddToHeader($sKey, $sValue) + { + if (!array_key_exists('headers', $this->m_aData)) + { + $this->m_aData['headers'] = array(); + } + $this->m_aData['headers'][$sKey] = $sValue; + + if (strlen($sValue) > 0) + { + $oHeaders = $this->m_oMessage->getHeaders(); + switch(strtolower($sKey)) + { + case 'return-path': + $this->m_oMessage->setReturnPath($sValue); + break; + + default: + $oHeaders->addTextHeader($sKey, $sValue); + } + } + } + + public function SetMessageId($sId) + { + $this->m_aData['message_id'] = $sId; + + // Note: Swift will add the angle brackets for you + // so let's remove the angle brackets if present, for historical reasons + $sId = str_replace(array('<', '>'), '', $sId); + + $oMsgId = $this->m_oMessage->getHeaders()->get('Message-ID'); + $oMsgId->SetId($sId); + } + + public function SetReferences($sReferences) + { + $this->AddToHeader('References', $sReferences); + } + + public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null) + { + if (($sMimeType === 'text/html') && ($sCustomStyles !== null)) + { + $oDomDocument = CssInliner::fromHtml($sBody)->inlineCss($sCustomStyles)->getDomDocument(); + HtmlPruner::fromDomDocument($oDomDocument)->removeElementsWithDisplayNone(); + $sBody = CssToAttributeConverter::fromDomDocument($oDomDocument)->convertCssToVisualAttributes()->render(); // Adds html/body tags if not already present + } + $this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType); + $this->m_oMessage->setBody($sBody, $sMimeType); + } + + public function AddPart($sText, $sMimeType = 'text/html') + { + if (!array_key_exists('parts', $this->m_aData)) + { + $this->m_aData['parts'] = array(); + } + $this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType); + $this->m_oMessage->addPart($sText, $sMimeType); + } + + public function AddAttachment($data, $sFileName, $sMimeType) + { + if (!array_key_exists('attachments', $this->m_aData)) + { + $this->m_aData['attachments'] = array(); + } + $this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType); + $this->m_oMessage->attach(new Swift_Attachment($data, $sFileName, $sMimeType)); + } + + public function SetSubject($sSubject) + { + $this->m_aData['subject'] = $sSubject; + $this->m_oMessage->setSubject($sSubject); + } + + public function GetSubject() + { + return $this->m_oMessage->getSubject(); + } + + /** + * Helper to transform and sanitize addresses + * - get rid of empty addresses + */ + protected function AddressStringToArray($sAddressCSVList) + { + $aAddresses = array(); + foreach(explode(',', $sAddressCSVList) as $sAddress) + { + $sAddress = trim($sAddress); + if (strlen($sAddress) > 0) + { + $aAddresses[] = $sAddress; + } + } + return $aAddresses; + } + + public function SetRecipientTO($sAddress) + { + $this->m_aData['to'] = $sAddress; + if (!empty($sAddress)) + { + $aAddresses = $this->AddressStringToArray($sAddress); + $this->m_oMessage->setTo($aAddresses); + } + } + + public function GetRecipientTO($bAsString = false) + { + $aRes = $this->m_oMessage->getTo(); + if ($aRes === null) + { + // There is no "To" header field + $aRes = array(); + } + if ($bAsString) + { + $aStrings = array(); + foreach ($aRes as $sEmail => $sName) + { + if (is_null($sName)) + { + $aStrings[] = $sEmail; + } + else + { + $sName = str_replace(array('<', '>'), '', $sName); + $aStrings[] = "$sName <$sEmail>"; + } + } + return implode(', ', $aStrings); + } + else + { + return $aRes; + } + } + + public function SetRecipientCC($sAddress) + { + $this->m_aData['cc'] = $sAddress; + if (!empty($sAddress)) + { + $aAddresses = $this->AddressStringToArray($sAddress); + $this->m_oMessage->setCc($aAddresses); + } + } + + public function SetRecipientBCC($sAddress) + { + $this->m_aData['bcc'] = $sAddress; + if (!empty($sAddress)) + { + $aAddresses = $this->AddressStringToArray($sAddress); + $this->m_oMessage->setBcc($aAddresses); + } + } + + public function SetRecipientFrom($sAddress, $sLabel = '') + { + $this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel); + if ($sLabel != '') + { + $this->m_oMessage->setFrom(array($sAddress => $sLabel)); + } + else if (!empty($sAddress)) + { + $this->m_oMessage->setFrom($sAddress); + } + } + + public function SetRecipientReplyTo($sAddress) + { + $this->m_aData['reply_to'] = $sAddress; + if (!empty($sAddress)) + { + $this->m_oMessage->setReplyTo($sAddress); + } + } + +} + +///////////////////////////////////////////////////////////////////////////////////// + +/** + * Extension to SwiftMailer: "debug" transport that pretends messages have been sent, + * but just log them to a file. + * + * @package Swift + * @author Denis Flaven + */ +class Swift_Transport_LogFileTransport extends Swift_Transport_NullTransport +{ + protected $sLogFile; + + /** + * @inheritDoc + */ + public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null) + { + $hFile = @fopen($this->sLogFile, 'a'); + if ($hFile) { + $sTxt = "================== ".date('Y-m-d H:i:s')." ==================\n"; + $sTxt .= $message->toString()."\n"; + + @fwrite($hFile, $sTxt); + @fclose($hFile); + } + + return parent::send($message, $failedRecipients); + } + + public function setLogFile($sFilename) + { + $this->sLogFile = $sFilename; + } +} + +/** + * Pretends messages have been sent, but just log them to a file. + * + * @package Swift + * @author Denis Flaven + */ +class Swift_LogFileTransport extends Swift_Transport_LogFileTransport +{ + /** + * Create a new LogFileTransport. + */ + public function __construct(Swift_Events_EventDispatcher $eventDispatcher) + { + parent::__construct($eventDispatcher); + call_user_func_array( + array($this, 'Swift_Transport_LogFileTransport::__construct'), + Swift_DependencyContainer::getInstance() + ->createDependenciesFor('transport.null') + ); + } + + /** + * Create a new LogFileTransport instance. + * + * @return Swift_LogFileTransport + */ + public static function newInstance() + { + return new self(); + } +} \ No newline at end of file diff --git a/templates/pages/backoffice/oauth/Wizard.html.twig b/templates/pages/backoffice/oauth/Wizard.html.twig new file mode 100644 index 000000000..d13590525 --- /dev/null +++ b/templates/pages/backoffice/oauth/Wizard.html.twig @@ -0,0 +1,48 @@ +{# @copyright Copyright (C) 2010-2022 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +
+

{{ 'UI:OAuth:Wizard:Page:Title'|dict_s }}

+ +
+ +
+
+ {% set sIsChecked = 'checked' %} + {% for aSelect in aProviders %} + + + + {% set sIsChecked = '' %} + {% endfor %} +
+
+ +
+ {{ 'UI:OAuth:Wizard:Form:Panel:Title'|dict_s }} +
+ + {% for sName, aInput in aInputs %} +
+
+
+
+ +
+
+
+ {% endfor %} +
+
+
+ + +
\ No newline at end of file