From d975cfafd7dccbff676a3cc93d4abec583367552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ANDRE=20s=C3=A9bastien?= <sandre@afi-sa.fr> Date: Wed, 8 Jul 2020 17:23:17 +0200 Subject: [PATCH] dev#110690 : Add CAS Identity provider --- VERSIONS_WIP/110690 | 1 + .../opac/controllers/AuthController.php | 29 +- .../opac/views/scripts/auth/boite-login.phtml | 10 +- .../opac/views/scripts/auth/login.phtml | 9 +- library/Class/Auth/IdentityProvider.php | 37 +- library/Class/IdentityProvider.php | 46 +- library/Class/IdentityProvider/Cas2.php | 34 + library/Class/IdentityProvider/Types.php | 3 +- .../TableDescription/IdentityProviders.php | 44 ++ library/Class/Template.php | 27 +- library/Class/User/Identity.php | 10 + library/Class/WebService/Acheteza.php | 27 - library/Class/WebService/Cas2.php | 96 +++ library/Class/WebService/IdentityProvider.php | 23 +- library/Class/WebService/OpenId.php | 17 +- library/ZendAfi/Controller/Action.php | 5 + .../ZendAfi/Form/Admin/IdentityProvider.php | 25 +- .../Abonne/AssociatedProvidersBoard.php | 19 +- .../ZendAfi/View/Helper/Admin/HelpLink.php | 3 +- .../ZendAfi/View/Helper/IdentityProviders.php | 7 +- library/ZendAfi/View/Helper/RenderLogin.php | 34 + .../View/Wrapper/RichContent/Section.php | 15 +- .../Wrapper/User/RichContent/Settings.php | 132 ++-- .../Widget/Accessibility/Definition.php | 6 + .../Library/Widget/AdminTools/Definition.php | 6 + .../Library/Widget/Breadcrumb/Definition.php | 7 + .../Widget/Carousel/Agenda/Definition.php | 11 +- .../Widget/Carousel/Article/Definition.php | 10 + .../Widget/Carousel/Author/Definition.php | 8 + .../Library/Widget/Carousel/Definition.php | 109 +++- .../Widget/Carousel/Domain/Definition.php | 9 + .../Widget/Carousel/Library/Definition.php | 7 + .../Widget/Carousel/Newsletter/Definition.php | 8 + .../Widget/Carousel/Record/Definition.php | 9 + .../Widget/Carousel/Review/Definition.php | 5 + .../Widget/Carousel/Rss/Definition.php | 8 + .../Library/Widget/Credits/Definition.php | 6 + .../Library/Widget/Free/Definition.php | 6 + .../Widget/IdentityProvider/Definition.php | 42 ++ .../Library/Widget/Image/Definition.php | 6 + .../Library/Widget/Language/Definition.php | 5 + .../Library/Widget/Login/Definition.php | 6 + .../Intonation/Library/Widget/Login/View.php | 3 +- .../Library/Widget/Menu/Definition.php | 6 + .../Library/Widget/Nav/Definition.php | 7 + .../Library/Widget/Notify/Definition.php | 7 + .../Library/Widget/Scroll/Definition.php | 6 + .../Library/Widget/Search/Definition.php | 10 +- .../Library/Widget/Share/Definition.php | 6 + .../Library/Widget/TemplatesAware.php | 35 ++ .../Library/Widget/TemplatesAwareNoHeader.php | 35 ++ .../Intonation/Library/WidgetTemplates.php | 473 +++++++++----- library/templates/Intonation/Template.php | 4 +- .../Intonation/View/IdentityProviders.php | 27 + .../templates/Intonation/View/RenderLogin.php | 49 ++ .../Intonation/View/Widget/Login.php | 1 + .../TEMPLATE_IDENTITY_PROVIDER.jpg | Bin 0 -> 22202 bytes .../js/widget_templates/TEMPLATE_LANGUE.jpg | Bin 0 -> 11804 bytes .../modules/AbstractControllerTestCase.php | 1 + .../DriveCheckOutBookingTest.php | 4 +- .../IdentityProviderAdminTest.php | 71 +++ .../IdentityProviderAuthenticationCasTest.php | 585 ++++++++++++++++++ .../IdentityProviderAuthenticationTest.php | 20 +- tests/scenarios/PnbDilicom/PnbDilicomTest.php | 2 +- .../RGPD/PatronDownloadDatasTest.php | 15 +- .../Templates/TemplatesAddWidgetTest.php | 79 +++ .../Templates/TemplatesAuthLoginTest.php | 73 +++ .../Templates/TemplatesAuthorTest.php | 8 +- .../TemplatesPatronConfigurationsTest.php | 84 +++ tests/scenarios/Templates/TemplatesTest.php | 26 +- .../TemplatesWidgetIdentityProvidersTest.php | 50 ++ .../Templates/TemplatesWidgetTest.php | 26 - 72 files changed, 2213 insertions(+), 427 deletions(-) create mode 100644 VERSIONS_WIP/110690 create mode 100644 library/Class/IdentityProvider/Cas2.php create mode 100644 library/Class/TableDescription/IdentityProviders.php create mode 100644 library/Class/WebService/Cas2.php create mode 100644 library/ZendAfi/View/Helper/RenderLogin.php create mode 100644 library/templates/Intonation/Library/Widget/IdentityProvider/Definition.php create mode 100644 library/templates/Intonation/Library/Widget/TemplatesAware.php create mode 100644 library/templates/Intonation/Library/Widget/TemplatesAwareNoHeader.php create mode 100644 library/templates/Intonation/View/IdentityProviders.php create mode 100644 library/templates/Intonation/View/RenderLogin.php create mode 100644 public/opac/js/widget_templates/TEMPLATE_IDENTITY_PROVIDER.jpg create mode 100644 public/opac/js/widget_templates/TEMPLATE_LANGUE.jpg create mode 100644 tests/scenarios/IdentityProvider/IdentityProviderAuthenticationCasTest.php create mode 100644 tests/scenarios/Templates/TemplatesAddWidgetTest.php create mode 100644 tests/scenarios/Templates/TemplatesAuthLoginTest.php create mode 100644 tests/scenarios/Templates/TemplatesPatronConfigurationsTest.php create mode 100644 tests/scenarios/Templates/TemplatesWidgetIdentityProvidersTest.php diff --git a/VERSIONS_WIP/110690 b/VERSIONS_WIP/110690 new file mode 100644 index 00000000000..1adf4f022e4 --- /dev/null +++ b/VERSIONS_WIP/110690 @@ -0,0 +1 @@ + - ticket #110690 : Compte lecteur : Ajout de la prise en charge de serveurs d'identité CAS 2.0 \ No newline at end of file diff --git a/application/modules/opac/controllers/AuthController.php b/application/modules/opac/controllers/AuthController.php index c19f4034b14..dfc6a53165c 100644 --- a/application/modules/opac/controllers/AuthController.php +++ b/application/modules/opac/controllers/AuthController.php @@ -161,16 +161,12 @@ class AuthController extends ZendAfi_Controller_Action { public function loginAction() { + $preferences = Class_Template::current() + ->filterNotNamespaced($this->_loginPrefFromWidgetOrModule()); - $this->view->preferences = $this->_loginPrefFromWidgetOrModule(); $redirect = $this->_getParam('redirect', Class_Url::relative(['module' => 'opac', 'action' => 'index', 'controller' => 'index'])); - $this->view->redirect = $redirect; - $service = $this->_getParam('service',''); - $this->view->service = $service; - $this->view->titreAdd($this->view->_('Connexion')); - $strategy = Class_Auth_Strategy::newFor($this); $strategy->setDefaultUrl($redirect); $strategy @@ -179,11 +175,30 @@ class AuthController extends ZendAfi_Controller_Action { $user->registerNotificationsOn($this->getHelper('notify')->bePopup()); }); + $service = $this->_getParam('service', ''); + + $url_action = $this->view->url(($form_action = $strategy->getFormAction($this->view->id_module)) + ? $form_action + : ['controller' => 'auth', + 'action' => ('boite-login'), + 'id_module' => $this->view->id_module]); + + $datas = $this->_getInspector() + ->addToParams(['redirect_url' => $redirect, + 'service' => $service, + 'id_notice' => $this->view->id_notice]); + + $settings = array_merge(['Preferences' => $preferences], + ['FormOptions' => ['data' => array_merge($preferences, $datas), + 'action' => $url_action]]); + $strategy->processLogin(); + $this->view->titreAdd($this->_('Connexion')); + $this->view->preferences = $preferences; $this->view->title = $strategy->getPageTitle($this->view); $this->view->message = $strategy->getMessage($this->view); - $this->view->form_action = $this->view->url($strategy->getFormAction($this->view->id_module)); + $this->view->settings = new Class_Entity($settings); } diff --git a/application/modules/opac/views/scripts/auth/boite-login.phtml b/application/modules/opac/views/scripts/auth/boite-login.phtml index d109856ab09..81450e47bc3 100644 --- a/application/modules/opac/views/scripts/auth/boite-login.phtml +++ b/application/modules/opac/views/scripts/auth/boite-login.phtml @@ -1,8 +1,10 @@ <?php -$url_action = $this->form_action ? - $this->form_action : $this->url(['controller' => 'auth', - 'action' => ('boite-login'), - 'id_module' => $this->id_module]); +$url_action = $this->form_action + ? $this->form_action + : $this->url(['controller' => 'auth', + 'action' => ('boite-login'), + 'id_module' => $this->id_module]); + $datas = ['redirect_url' => $this->redirect, 'service' => $this->service, 'id_notice' => $this->id_notice]; diff --git a/application/modules/opac/views/scripts/auth/login.phtml b/application/modules/opac/views/scripts/auth/login.phtml index a8399533032..b310a0caccf 100644 --- a/application/modules/opac/views/scripts/auth/login.phtml +++ b/application/modules/opac/views/scripts/auth/login.phtml @@ -1,9 +1,2 @@ <?php -$this->openBoite($this->title); - -if ($this->message) - echo $this->tag('p', $this->message); - -include('boite-login.phtml'); -$this->closeBoite(); -?> +echo $this->renderLogin($this->settings, $this->title, $this->message); diff --git a/library/Class/Auth/IdentityProvider.php b/library/Class/Auth/IdentityProvider.php index 44c84158372..66aa661c040 100644 --- a/library/Class/Auth/IdentityProvider.php +++ b/library/Class/Auth/IdentityProvider.php @@ -52,13 +52,16 @@ class Class_Auth_IdentityProviderLogged extends Class_Auth_Logged { protected function _handleRedirect() { - if ((!$provider = $this->_getProvider()) - || !$provider->associate(Class_Users::getIdentity())) + if (!$provider = $this->_getProvider()) return parent::_handleRedirect(); - $this->setRedirectUrl(($url = $provider->loginSuccessRedirectUrl()) - ? $url - : $this->default_url); + if (!$provider->associate(Class_Users::getIdentity()) + && ($message = $provider->getLastMessage())) + $this->controller->notify($message); + + $this->redirect_url = ($url = $provider->loginSuccessRedirectUrl()) + ? $url + : $this->default_url; return parent::_handleRedirect(); } @@ -130,16 +133,28 @@ class Class_Auth_IdentityProviderNotLogged extends Class_Auth_NotLogged { protected function _handleRedirect() { - if ((!$provider = $this->_getProvider())) - return parent::_handleRedirect(); + $provider = $this->_getProvider(); + $user = $provider ? $provider->getRemotelyLoggedUser() : null; + $message = $provider ? $provider->getLastMessage() : null; + + if ($user || $message) + $this->_redirectToProviderSuccessOrDefaultUrl($provider); - if ($user = $provider->getRemotelyLoggedUser()) { + if ($user) ZendAfi_Auth::getInstance()->logUser($user); - $this->redirect_url = ($url = $provider->loginSuccessRedirectUrl()) + + if ($message) + $this->controller->notify($message); + + return parent::_handleRedirect(); + } + + + protected function _redirectToProviderSuccessOrDefaultUrl($provider) { + $this->redirect_url = ($url = $provider->loginSuccessRedirectUrl()) ? $url : $this->default_url; - } - return parent::_handleRedirect(); + return $this; } } diff --git a/library/Class/IdentityProvider.php b/library/Class/IdentityProvider.php index 6b9562dcbcc..d247c2f3db3 100644 --- a/library/Class/IdentityProvider.php +++ b/library/Class/IdentityProvider.php @@ -36,6 +36,8 @@ class IdentityProviderLoader extends Storm_Model_Loader { class Class_IdentityProvider extends Storm_Model_Abstract{ + use Trait_Translator, Trait_LastMessage; + protected $_table_name = 'identity_provider', $_loader_class = 'IdentityProviderLoader', @@ -50,19 +52,29 @@ class Class_IdentityProvider extends Storm_Model_Abstract{ 'dependents' => 'delete'] ], $_config_fields = ['url', - 'url_api', 'client_id', 'client_secret', 'nonce', 'logout_url', 'button_login', 'button_logout', - 'prod_url' ], + 'prod_url', + 'associate_on_login'], $_config_as_array, $_context; + public function validate() { + $this->checkAttribute('client_id', + $this->hasClientId() || $this->getType() == 'cas2', + $this->_('Identifiant client est obligatoire')); + $this->checkAttribute('client_secret', + $this->hasClientSecret() || $this->getType() == 'cas2', + $this->_('Clé secrète est obligatoire')); + } + + public function setContext($context) { $this->_context = $context; return $this; @@ -82,7 +94,6 @@ class Class_IdentityProvider extends Storm_Model_Abstract{ public function isAttachable() { $user = Class_Users::getIdentity(); return !($user && $this->isRemotelyLogged()); -// && !$this->isAssociatedTo($user); } @@ -119,11 +130,27 @@ class Class_IdentityProvider extends Storm_Model_Abstract{ public function getRemotelyLoggedUser() { - return (($identifier = $this->getRemoteUserId()) + $user = (($identifier = $this->getRemoteUserId()) && ($identity = Class_User_Identity::findFirstBy(['provider_id' => $this->getId(), 'identifier' => $identifier]))) ? $identity->getUser() : null; + + if ($user || !$this->getAssociateOnLogin()) + return $user; + + if (1 !== ($count = Class_Users::countBy(['login' => $identifier]))) { + $this->_error($this->_plural($count, + 'Connection impossible, l\'identifiant n\'existe pas', + '', + 'Connection impossible, l\'identifiant est dupliqué')); + return null; + } + + $user = Class_Users::findFirstBy(['login' => $identifier]); + + Class_User_Identity::findOrCreateFor($user, $this, $identifier); + return $user; } @@ -134,6 +161,11 @@ class Class_IdentityProvider extends Storm_Model_Abstract{ if (!$identifier = $this->getRemoteUserId()) return false; + if (Class_User_Identity::isAssociatedToAnother($user, $this, $identifier)) { + $this->_error($this->_('L\'association a échoué, cette identité est déjà associée à un autre compte')); + return false; + } + return Class_User_Identity::findOrCreateFor($user, $this, $identifier); } @@ -145,8 +177,10 @@ class Class_IdentityProvider extends Storm_Model_Abstract{ protected function _doWithValidatedType($closure) { $type = $this->getTypeClass(); - $type->validate($this->_context); - return $closure($type); + try { + $type->validate($this->_context); + return $closure($type); + } catch (Exception $e) {} } diff --git a/library/Class/IdentityProvider/Cas2.php b/library/Class/IdentityProvider/Cas2.php new file mode 100644 index 00000000000..fd24fdf50a2 --- /dev/null +++ b/library/Class/IdentityProvider/Cas2.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +class Class_IdentityProvider_Cas2 extends Class_IdentityProvider_Default { + protected $_service_class = 'Class_WebService_Cas2'; + + public function getType() { + return 'cas2'; + } + + + public function setLoginSuccessRedirectUrl($url) { + return $this; + } +} diff --git a/library/Class/IdentityProvider/Types.php b/library/Class/IdentityProvider/Types.php index db7c540de40..50704cb784f 100644 --- a/library/Class/IdentityProvider/Types.php +++ b/library/Class/IdentityProvider/Types.php @@ -34,7 +34,8 @@ class Class_IdentityProvider_Types { return ['default' => $this->_('OpenId Connect'), 'google' => $this->_('Google (OpenId)'), 'franceconnect' => $this->_('Franceconnect'), - 'acheteza' => $this->_('Portail citoyen Acheteza.com')]; + 'acheteza' => $this->_('Portail citoyen Acheteza.com'), + 'cas2' => $this->_('CAS 2.0')]; } diff --git a/library/Class/TableDescription/IdentityProviders.php b/library/Class/TableDescription/IdentityProviders.php new file mode 100644 index 00000000000..8fb58f78274 --- /dev/null +++ b/library/Class/TableDescription/IdentityProviders.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +class Class_TableDescription_IdentityProviders extends Class_TableDescription { + public function init() { + $this->addColumn($this->_('Nom du fournisseur d\'identité'), + function($model) + { + return $model->getProvider() + ? $model->getProvider()->getLibelle() + : null; + }) + ->addColumn($this->_('Action'), + function($model, $attrib, $canvas) + { + return $canvas->getView() + ->tagAnchor(['controller' => 'abonne', + 'action' => 'dissociate-provider', + 'id' => $model->getId() + ], + $this->_('Dissocier votre compte')); + }); + + } +} diff --git a/library/Class/Template.php b/library/Class/Template.php index 7c31f7062e4..63ff1ce6799 100644 --- a/library/Class/Template.php +++ b/library/Class/Template.php @@ -34,7 +34,8 @@ class Class_Template { $_settings, $_control_key, $_patcher, - $_icons_cache; + $_icons_cache, + $_namespace; public function __call($name, $params) { @@ -220,7 +221,29 @@ class Class_Template { public function withNameSpace($text) { - return Storm_Inflector::camelize($this->getId() . '_' . Storm_Inflector::underscorize($text)); + return $this->_getNameSpace() . Storm_Inflector::camelize(Storm_Inflector::underscorize($text)); + } + + + public function isNamespaced($name) { + return 0 === strpos($name, $this->_getNameSpace()); + } + + + protected function _getNameSpace() { + return $this->_namespace + ? $this->_namespace + : $this->_namespace = Storm_Inflector::camelize($this->getId()); + } + + + public function filterNotNamespaced($items) { + return array_filter($items, + function($item) + { + return !$this->isNamespaced($item); + }, + ARRAY_FILTER_USE_KEY); } diff --git a/library/Class/User/Identity.php b/library/Class/User/Identity.php index 85c8260893a..0f4bd4244e3 100644 --- a/library/Class/User/Identity.php +++ b/library/Class/User/Identity.php @@ -38,6 +38,16 @@ class Class_User_IdentityLoader extends Storm_Model_Loader { } + public function isAssociatedToAnother($user, $provider, $identifier) { + if (!$user || !$provider || !$identifier) + return false; + + return 0 < Class_User_Identity::countBy(['provider_id' => (int) $provider->getId(), + 'identifier' => $identifier, + 'user_id not' => (int) $user->getId()]); + } + + public function findIdentitiesForActiveProviders($user) { return array_filter(array_map(function($provider) use ($user) { diff --git a/library/Class/WebService/Acheteza.php b/library/Class/WebService/Acheteza.php index c6b7acb7b74..f7b849f75e4 100644 --- a/library/Class/WebService/Acheteza.php +++ b/library/Class/WebService/Acheteza.php @@ -65,21 +65,6 @@ class Class_WebService_Acheteza extends Class_WebService_IdentityProvider { } - protected function _providerUrlTo($action) { - return $this->_trailingSlashUrl($this->_provider->getUrl()) . $action; - } - - - protected function _providerUrlApiTo($action) { - return $this->_trailingSlashUrl($this->_provider->getUrlApi()) . $action; - } - - - protected function _trailingSlashUrl($url) { - return $url . ('/' !== substr($url, -1) ? '/' : ''); - } - - protected function _requestToken() { $response = $this->_postRequest('GetRequestToken', ['grant_type' => 'client_credentials']); @@ -115,16 +100,4 @@ class Class_WebService_Acheteza extends Class_WebService_IdentityProvider { 'application/x-www-form-urlencoded', ['headers' => $headers]); } - - - protected function _getRemoteId($token) { - $response = $this->getWebClient()->open_url($this->_providerUrlApiTo('me'), - ['headers' => ['Authorization: Bearer '. $token]]); - - return ($response - && ($json = json_decode($response)) - && isset($json->id)) - ? $json->id - : null; - } } diff --git a/library/Class/WebService/Cas2.php b/library/Class/WebService/Cas2.php new file mode 100644 index 00000000000..bc54cda51a8 --- /dev/null +++ b/library/Class/WebService/Cas2.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +class Class_WebService_Cas2 extends Class_WebService_IdentityProvider { + + public function validate($context) { + if (!$ticket = $context->getParam('ticket')) + return $this; + + if ($this->_isConsumed($ticket)) + return $this; + + $this->_consume($ticket); + + $response = $this->getWebClient() + ->getResponse($this->_providerUrlTo('serviceValidate') + . '?' + . http_build_query(['ticket' => $ticket, + 'service' => $this->_serviceUrl()])); + + if ($response->isError() + || (!$body = $response->getBody()) + || false === strpos($body, '<cas:authenticationSuccess>') + || (!$user = (string)array_filter((new SimpleXMLElement($body))->xpath('//cas:user'))[0]) + ) { + $this->clearSession(); + return $this; + } + + $this->loginWith($user); + return $this; + } + + + protected function _consume($ticket) { + $tickets = isset($this->getSession()->tickets) + ? $this->getSession()->tickets + : []; + + $tickets[] = $ticket; + $this->getSession()->tickets = array_unique($tickets); + + return $this; + } + + + protected function _isConsumed($ticket) { + $tickets = isset($this->getSession()->tickets) + ? $this->getSession()->tickets + : []; + + return in_array($ticket, $tickets); + } + + + public function getAuthorizeUrl() { + return $this->_providerUrlTo('login') + . '?' + . http_build_query(['service' => $this->_serviceUrl()]); + } + + + public function getLogoutUrl($redirect) { + return $this->_providerUrlTo('logout') + . '?' + . http_build_query(['service' => Class_Url::absolute([], null, true)]); + } + + + protected function _serviceUrl() { + return Class_Url::absolute(['controller' => 'auth', + 'action' => 'login', + 'provider' => $this->_provider->getId()], + null, + true); + } +} \ No newline at end of file diff --git a/library/Class/WebService/IdentityProvider.php b/library/Class/WebService/IdentityProvider.php index 868b2a3db46..ad3c3becefa 100644 --- a/library/Class/WebService/IdentityProvider.php +++ b/library/Class/WebService/IdentityProvider.php @@ -23,9 +23,9 @@ abstract class Class_WebService_IdentityProvider { use Trait_SimpleWebClient, Trait_TimeSource; - /** @var array of ZendAfi_Session_Namespace */ - protected static $_session_namespace = [], - $_expiration_delay = 300; + protected static + $_session_namespace = [], // @var array of ZendAfi_Session_Namespace + $_expiration_delay = 300; /** @var Class_IdentityProvider */ protected $_provider; @@ -33,7 +33,6 @@ abstract class Class_WebService_IdentityProvider { /** @return ZendAfi_Session_Namespace */ public static function getSession() { - $called_class = get_called_class(); if (isset(static::$_session_namespace[$called_class])) return static::$_session_namespace[$called_class]; @@ -130,8 +129,24 @@ abstract class Class_WebService_IdentityProvider { return $this; } + /** @return string */ public function getLogoutUrl($redirect) { return ''; } + + + protected function _providerUrlTo($action) { + return $this->_trailingSlashUrl($this->_provider->getUrl()) . $action; + } + + + protected function _providerUrlApiTo($action) { + return $this->_trailingSlashUrl($this->_provider->getUrlApi()) . $action; + } + + + protected function _trailingSlashUrl($url) { + return $url . ('/' !== substr($url, -1) ? '/' : ''); + } } diff --git a/library/Class/WebService/OpenId.php b/library/Class/WebService/OpenId.php index 99d35982061..5fbddbfd80c 100644 --- a/library/Class/WebService/OpenId.php +++ b/library/Class/WebService/OpenId.php @@ -65,7 +65,7 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider { $this->_client_id = $provider->getClientId(); $this->_client_secret = $provider->getClientSecret(); - $this->_nonce = true;//$provider->getNonce(); + $this->_nonce = true; $this->_url = $provider->getUrl(); $this->_logout_url = $provider->getLogoutUrl() ? $provider->getLogoutUrl() @@ -199,8 +199,8 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider { $this->getSession()->access_token = $json->access_token; if (isset($json->expires_in)) { - $this->setExpirationDelay($json->expires_in); - $this->getSession()->expires_in = $json->expires_in; + $this->setExpirationDelay($json->expires_in); + $this->getSession()->expires_in = $json->expires_in; } $this->getSession()->token = $json->id_token; @@ -268,7 +268,7 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider { } - protected function _getRandomToken(){ + protected function _getRandomToken() { return $this->getPhpCommand()->sha1(mt_rand(0, mt_getrandmax())); } @@ -295,7 +295,6 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider { 'scope' => 'openid', 'state' => $this->getState()]; -// if ($this->_nonce) $params['nonce'] = $this->getNonce(); return $this->_authorization_url . '?' . http_build_query($params); @@ -303,8 +302,10 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider { public function getLogoutUrl($redirect_url) { - return $this->_logout_url . '?' . http_build_query(['id_token_hint' => $this->_getToken(), - 'state' => $this->getState(), - 'post_logout_redirect_uri' => $this->_getBokehLogoutUrl()]); + return $this->_logout_url + . '?' + . http_build_query(['id_token_hint' => $this->_getToken(), + 'state' => $this->getState(), + 'post_logout_redirect_uri' => $this->_getBokehLogoutUrl()]); } } diff --git a/library/ZendAfi/Controller/Action.php b/library/ZendAfi/Controller/Action.php index 7f01e399844..ffb803b6019 100644 --- a/library/ZendAfi/Controller/Action.php +++ b/library/ZendAfi/Controller/Action.php @@ -401,4 +401,9 @@ class ZendAfi_Controller_Action_NullInspector { public function addButton($definition) { return $this; } + + + public function addToParams($datas) { + return $datas; + } } diff --git a/library/ZendAfi/Form/Admin/IdentityProvider.php b/library/ZendAfi/Form/Admin/IdentityProvider.php index 8282bdac1ec..e3fc75569c7 100644 --- a/library/ZendAfi/Form/Admin/IdentityProvider.php +++ b/library/ZendAfi/Form/Admin/IdentityProvider.php @@ -25,8 +25,11 @@ class ZendAfi_Form_Admin_IdentityProvider extends ZendAfi_Form { parent::init(); Class_ScriptLoader::getInstance() - ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#url, #url_api, #button_logout, #logout_url").closest("tr"), ["default", "acheteza"]);') - ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#prod_url").closest("tr"), ["franceconnect"]);'); + ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#button_logout, #logout_url").closest("tr"), ["default", "acheteza"]);') + ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#url").closest("tr"), ["cas2", "default", "acheteza"]);') + ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#prod_url").closest("tr"), ["franceconnect"]);') + ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#client_id, #client_secret").closest("tr"), ["default", "google", "franceconnect", "acheteza"]);') + ->addJQueryBackEnd('formSelectToggleVisibilityForElement("#type", $("#associate_on_login").closest("tr"), ["cas2"]);'); $this @@ -49,16 +52,12 @@ class ZendAfi_Form_Admin_IdentityProvider extends ZendAfi_Form { ->addElement('text', 'client_id', ['label' => $this->_('Identifiant client'), - 'size' => 50, - 'required' => true, - 'allowEmpty' => false]) + 'size' => 50]) ->addElement('text', 'client_secret', ['label' => $this->_('Clé secrète'), - 'size' => 50, - 'required' => true, - 'allowEmpty' => false]) + 'size' => 50]) ->addElement('url', 'url', @@ -67,12 +66,6 @@ class ZendAfi_Form_Admin_IdentityProvider extends ZendAfi_Form { 'allowEmpty' => false, 'title' => $this->_('URL SSO du fournisseur d\'identité')]) - ->addElement('url', - 'url_api', - ['label' => $this->_('URL API'), - 'size' => 50, - 'title' => $this->_('URL API du fournisseur d\'identité')]) - ->addElement('textarea', 'button_logout', ['label' => $this->_('Html de logout'), @@ -89,6 +82,10 @@ class ZendAfi_Form_Admin_IdentityProvider extends ZendAfi_Form { ['label' => $this->_('En production'), 'title' => $this->_('En production')]) + ->addElement('checkbox', + 'associate_on_login', + ['label' => $this->_('Association automatique sur l\'identifiant')]) + ->addUniqDisplayGroup('provider'); } } diff --git a/library/ZendAfi/View/Helper/Abonne/AssociatedProvidersBoard.php b/library/ZendAfi/View/Helper/Abonne/AssociatedProvidersBoard.php index 0d247bc8b27..3ff91958103 100644 --- a/library/ZendAfi/View/Helper/Abonne/AssociatedProvidersBoard.php +++ b/library/ZendAfi/View/Helper/Abonne/AssociatedProvidersBoard.php @@ -42,24 +42,7 @@ class ZendAfi_View_Helper_Abonne_AssociatedProvidersBoard extends ZendAfi_View_H if (empty($this->_identities)) return $this->_tag('p', $this->_('Votre compte n\'est pas associé à un fournisseur d\'identité')); - $description = (new Class_TableDescription('active_identities')) - ->addColumn($this->_('Nom du fournisseur d\'identité'), - function($model) - { - return $model->getProvider() - ? $model->getProvider()->getLibelle() - : null; - }) - ->addColumn($this->_('Dissocier'), - function($model) - { - return $this->_tagAnchor(['controller' => 'abonne', - 'action' => 'dissociate-provider', - 'id' => $model->getId() - ], - $this->_('Dissocier votre compte')); - }); - + $description = (new Class_TableDescription_IdentityProviders('active_identities')); return $this->view->renderTable($description, $this->_identities); } diff --git a/library/ZendAfi/View/Helper/Admin/HelpLink.php b/library/ZendAfi/View/Helper/Admin/HelpLink.php index fb1dd64c225..fc6a303cabc 100644 --- a/library/ZendAfi/View/Helper/Admin/HelpLink.php +++ b/library/ZendAfi/View/Helper/Admin/HelpLink.php @@ -129,7 +129,8 @@ class ZendAfi_View_Helper_Admin_HelpLinkBokehWiki { 'emplacement' => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'], 'section' => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'], 'drive-checkout' => ['index' => 'Gérer_les_listes_de_rendez-vous_en_mode_drive', - 'plan' => 'Prise_de_rendez-vous_par_les_professionnels'] + 'plan' => 'Prise_de_rendez-vous_par_les_professionnels'], + 'identity-providers' => ['index' => 'Fournisseurs_d\'identités'], ]; diff --git a/library/ZendAfi/View/Helper/IdentityProviders.php b/library/ZendAfi/View/Helper/IdentityProviders.php index 24f75f8de8f..e4b18531119 100644 --- a/library/ZendAfi/View/Helper/IdentityProviders.php +++ b/library/ZendAfi/View/Helper/IdentityProviders.php @@ -29,9 +29,14 @@ class ZendAfi_View_Helper_IdentityProviders extends ZendAfi_View_Helper_BaseHelp array_map(function($provider) { if ($provider->isAttachable() || $provider->isLogged()) - return $this->view->Button_IdentityProvider($provider); + return $this->_renderButton($provider); }, Class_IdentityProvider::findAllActiveProviders() )); } + + + protected function _renderButton($provider) { + return $this->view->Button_IdentityProvider($provider); + } } diff --git a/library/ZendAfi/View/Helper/RenderLogin.php b/library/ZendAfi/View/Helper/RenderLogin.php new file mode 100644 index 00000000000..4b100227c29 --- /dev/null +++ b/library/ZendAfi/View/Helper/RenderLogin.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +class ZendAfi_View_Helper_RenderLogin extends ZendAfi_View_Helper_BaseHelper { + public function renderLogin($settings, $title, $message) { + $html = $this->view->openBoiteContent($title); + + if ($message) + $html .= $this->_tag('p', $message); + + $html .= $this->view->widget_Login($settings); + + return $html . $this->view->closeBoiteContent(); + } +} diff --git a/library/templates/Intonation/Library/View/Wrapper/RichContent/Section.php b/library/templates/Intonation/Library/View/Wrapper/RichContent/Section.php index 093d1d893eb..4687897f7e1 100644 --- a/library/templates/Intonation/Library/View/Wrapper/RichContent/Section.php +++ b/library/templates/Intonation/Library/View/Wrapper/RichContent/Section.php @@ -21,7 +21,6 @@ abstract class Intonation_Library_View_Wrapper_RichContent_Section { - use Trait_Translator; @@ -151,6 +150,20 @@ abstract class Intonation_Library_View_Wrapper_RichContent_Section { } + protected function _div() { + return call_user_func_array([$this->_view, 'div'], func_get_args()); + } + + + protected function _tag() { + return call_user_func_array([$this->_view, 'tag'], func_get_args()); + } + + + protected function _grid() { + return call_user_func_array([$this->_view, 'grid'], func_get_args()); + } + abstract public function getTitle(); abstract public function getContent(); abstract public function getClass(); diff --git a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php index 205f35de981..4c0f5d09f45 100644 --- a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php +++ b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Settings.php @@ -28,91 +28,81 @@ class Intonation_Library_View_Wrapper_User_RichContent_Settings extends Intonati public function getContent() { + $html = []; + + $html [] = $this->_renderIdentityProviders(); + $cards = new Class_User_Cards($this->_model); $count_cards = count($cards); - $parent_cards = $this->_model->getParentCards(); - $count_parent_cards = count($parent_cards); - - $html = [$this->_view->div(['class' => 'col-12'], - $this->_view->tag('h3', $this->_view->_plural($count_cards, - 'Aucune carte d\'abonné', - 'Ma carte d\'abonné', - 'Mes cartes d\'abonnés', - $count_cards))), - - $this->_view->div(['class' => 'col-12'], - $this->_renderCards($cards))]; - - if ($parent_cards) { - $html [] = $this->_view->div(['class' => 'col-12'], - $this->_view->tag('h3', $this->_view->_plural($count_parent_cards, - '', - 'Il gère mes prêts et mes réservations', - 'Ils gèrent mes prêts et mes réservations', - $count_parent_cards))); - $html [] = $this->_view->div(['class' => 'col-12'], - $this->_renderParentCards($parent_cards)); + $parent_cards = new Storm_Collection($this->_model->getParentCards()); + $count_parent_cards = $parent_cards->count(); + + $html [] = $this->_div(['class' => 'col-12'], + $this->_tag('h3', $this->_plural($count_cards, + 'Aucune carte d\'abonné', + 'Ma carte d\'abonné', + 'Mes cartes d\'abonnés'))); + + $html [] = $this->_div(['class' => 'col-12'], + $this->_renderCards($cards)); + + if ($count_parent_cards) { + $html [] = $this->_div(['class' => 'col-12'], + $this->_tag('h3', $this->_plural($count_parent_cards, + '', + 'Il gère mes prêts et mes réservations', + 'Ils gèrent mes prêts et mes réservations'))); + $html [] = $this->_div(['class' => 'col-12'], + $this->_renderParentCards($parent_cards)); } - $html [] = $this->_view->div(['class' => 'col-12 mt-5'], - $this->_view->tag('h3', $this->_('Mes paramètres'))); + $html [] = $this->_div(['class' => 'col-12 mt-5'], + $this->_tag('h3', $this->_('Mes paramètres'))); - $html [] = $this->_view->div(['class' => 'col-12'], - $this->_renderSettingsForm()); + $html [] = $this->_div(['class' => 'col-12'], + $this->_renderSettingsForm()); - return $this->_view->grid(implode($html)); + return $this->_grid(implode($html)); } protected function _renderCards($cards) { - $html = [$this->_view->div(['class' => 'col-12'], - $this->_view->tagAction(new Intonation_Library_Link(['Url' => $this->_view->url(['controller' => 'abonne', - 'action' => 'add-card']), - 'Text' => $this->_('Ajouter une carte'), - 'Image' => Class_Template::current()->getIco($this->_view, 'add_user', 'utils'), - 'Class' => 'btn btn-sm btn-success', - 'InlineText' => 1, - 'Popup' => 1])))]; + $link = new Intonation_Library_Link(['Url' => $this->_view->url(['controller' => 'abonne', + 'action' => 'add-card']), + 'Text' => $this->_('Ajouter une carte'), + 'Image' => Class_Template::current()->getIco($this->_view, 'add_user', 'utils'), + 'Class' => 'btn btn-sm btn-success', + 'InlineText' => 1, + 'Popup' => 1]); + + $html = [$this->_div(['class' => 'col-12'], + $this->_view->tagAction($link))]; if (!$cards->isEmpty()) - $html [] = $this->_view->div(['class' => 'mt-3 col-12'], - $this->_renderCardsCarousel($cards)); + $html [] = $this->_div(['class' => 'mt-3 col-12'], + $this->_renderCardsCarousel($cards, + 'Intonation_Library_View_Wrapper_Card')); - return $this->_view->grid(implode($html)); + return $this->_grid(implode($html)); } protected function _renderParentCards($cards) { - $html = $this->_view->div(['class' => 'col-12'], - $this->_renderParentCardsCarousel(new Storm_Collection($cards))); - - return $this->_view->grid($html); - } - - - protected function _renderCardsCarousel($cards) { - $cards = array_map(function($card) - { - return (new Intonation_Library_View_Wrapper_Card) - ->setModel($card) - ->setView($this->_view); - }, $cards->getArrayCopy()); - - $callback = function($wrapped) { - return $this->_view->cardify($wrapped); - }; + $html = $this->_div(['class' => 'col-12'], + $this->_renderCardsCarousel($cards, + 'Intonation_Library_View_Wrapper_ParentCard')); - return $this->_view->renderMultipleCarousel(new Storm_Collection($cards), $callback); + return $this->_grid($html); } - protected function _renderParentCardsCarousel($cards) { - $cards = array_map(function($card) - { - return (new Intonation_Library_View_Wrapper_ParentCard) - ->setModel($card) - ->setView($this->_view); - }, $cards->getArrayCopy()); + protected function _renderCardsCarousel($cards, $wrapper_class) { + $cards = array_map(function($card) use($wrapper_class) + { + return (new $wrapper_class) + ->setModel($card) + ->setView($this->_view); + }, $cards->getArrayCopy()); $callback = function($wrapped) { return $this->_view->cardify($wrapped); @@ -134,6 +124,20 @@ class Intonation_Library_View_Wrapper_User_RichContent_Settings extends Intonati } + protected function _renderIdentityProviders() { + if (!Class_AdminVar::isIdentityProvidersEnabled()) + return ''; + + $identities = Class_User_Identity::findIdentitiesForActiveProviders($this->_model); + $description = new Class_TableDescription_IdentityProviders('active_identities'); + $table = $this->_view->renderTable($description, $identities); + + return $this->_div(['class' => 'col-12'], + $this->_tag('h3', $this->_('Fournisseur(s) d\'identité associé(s)')) + . $table); + } + + public function getClass() { return 'user_settings'; } diff --git a/library/templates/Intonation/Library/Widget/Accessibility/Definition.php b/library/templates/Intonation/Library/Widget/Accessibility/Definition.php index cb491ba9db0..1664a166be9 100644 --- a/library/templates/Intonation/Library/Widget/Accessibility/Definition.php +++ b/library/templates/Intonation/Library/Widget/Accessibility/Definition.php @@ -21,6 +21,7 @@ class Intonation_Library_Widget_Accessibility_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; const CODE = 'ACCESSIBILITY', @@ -41,4 +42,9 @@ class Intonation_Library_Widget_Accessibility_Definition extends Class_Systeme_M 'display_colors' => 1, 'display_font_size' => 1]; } + + + protected function _templateTitle() { + return $this->_('Accessibilité'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/AdminTools/Definition.php b/library/templates/Intonation/Library/Widget/AdminTools/Definition.php index b1b32d2bba3..a5d4bb3485a 100644 --- a/library/templates/Intonation/Library/Widget/AdminTools/Definition.php +++ b/library/templates/Intonation/Library/Widget/AdminTools/Definition.php @@ -21,6 +21,7 @@ class Intonation_Library_Widget_AdminTools_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; const CODE = 'ADMIN_TOOLS', @@ -40,4 +41,9 @@ class Intonation_Library_Widget_AdminTools_Definition extends Class_Systeme_Modu $this->_defaultValues = ['titre' => $this->_libelle, 'display_mode' => static::TOGGLE]; } + + + protected function _templateTitle() { + return $this->_('Outils d\'administration'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Breadcrumb/Definition.php b/library/templates/Intonation/Library/Widget/Breadcrumb/Definition.php index 3bbad070164..0f856404644 100644 --- a/library/templates/Intonation/Library/Widget/Breadcrumb/Definition.php +++ b/library/templates/Intonation/Library/Widget/Breadcrumb/Definition.php @@ -21,6 +21,8 @@ class Intonation_Library_Widget_Breadcrumb_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; + const CODE = 'ARIANE'; protected @@ -35,4 +37,9 @@ class Intonation_Library_Widget_Breadcrumb_Definition extends Class_Systeme_Modu 'root' => 1, 'show_profile' => 1]; } + + + protected function _templateTitle() { + return $this->_('Fil d\'Ariane'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Agenda/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Agenda/Definition.php index 5fcff9df3f0..1995769d445 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Agenda/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Agenda/Definition.php @@ -20,7 +20,8 @@ */ -class Intonation_Library_Widget_Carousel_Agenda_Definition extends Intonation_Library_Widget_Carousel_Definition { +class Intonation_Library_Widget_Carousel_Agenda_Definition + extends Intonation_Library_Widget_Carousel_Definition { const CODE = 'CALENDAR', @@ -52,4 +53,12 @@ class Intonation_Library_Widget_Carousel_Agenda_Definition extends Intonation_Li return (new Class_Systeme_ModulesAccueil_Calendrier) ->getAvailableFilters(); } + + + protected function _templateLayouts() { + return [static::CAROUSEL, + static::MULTIPLE_CAROUSEL, + static::LISTING, + static::WALL]; + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Article/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Article/Definition.php index a996c480632..49a216b9153 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Article/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Article/Definition.php @@ -45,4 +45,14 @@ class Intonation_Library_Widget_Carousel_Article_Definition extends Intonation_L ['titre' => $this->_libelle, 'order' => static::SORT_RANDOM]); } + + + protected function _templateLayouts() { + return [static::CAROUSEL, + static::MULTIPLE_CAROUSEL, + static::LISTING, + static::HORIZONTAL_LISTING, + static::WALL]; + } + } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Author/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Author/Definition.php index 147c1e3e308..fda04dd2f6b 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Author/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Author/Definition.php @@ -41,4 +41,12 @@ class Intonation_Library_Widget_Carousel_Author_Definition extends Intonation_Li 'with_thumbnail' => true, 'authors_selection_mode' => Class_Systeme_ModulesAccueil_Authors::AUTHORS_SELECTION_MODE_ALL]); } + + + protected function _templateLayouts() { + return [static::CAROUSEL, + static::MULTIPLE_CAROUSEL, + static::LISTING, + static::WALL]; + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Definition.php index 69d9fe8cd58..e951247187a 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Definition.php @@ -37,18 +37,99 @@ class Intonation_Library_Widget_Carousel_Definition extends Class_Systeme_Module HORIZONTAL_CARD = 'card-horizontal', CARD = 'card'; - protected $_isPhone = false; - - - public function __construct() { - $this->_defaultValues = ['layout' => static::WALL, - 'rendering' => static::CARD_OVERLAY, - 'size' => 9, - 'rss' => false, - 'embeded_code' => false, - 'link_to_all' => false, - 'all_layout' => static::LISTING, - 'all_rendering' => static::HORIZONTAL_CARD - ]; - } + protected $_isPhone = false; + + + public function __construct() { + $this->_defaultValues = ['layout' => static::WALL, + 'rendering' => static::CARD_OVERLAY, + 'size' => 9, + 'rss' => false, + 'embeded_code' => false, + 'link_to_all' => false, + 'all_layout' => static::LISTING, + 'all_rendering' => static::HORIZONTAL_CARD + ]; + } + + + + public function templates($styles_callback) { + $templates = []; + foreach($this->_templateLayouts() as $name) + $templates[] = $this->_templateFor(static::CODE, $name, $styles_callback); + + return $templates; + } + + + protected function _templateLayouts() { + return []; + } + + + protected function _templateFor($type, $layout, $styles_callback) { + $definition = (new Intonation_Library_Widget_Carousel_DefinitionLayout)->newFor($layout); + + return ['title' => $definition->title(), + 'icon' => 'TEMPLATE_' . $type . '_' . $layout . '.jpg', + 'type' => $type, + 'configuration' => ['rendering' => $definition->rendering(), + 'layout' => $layout, + 'size' => $definition->size(), + 'boite' => $styles_callback()]]; + } +} + + + + +class Intonation_Library_Widget_Carousel_DefinitionLayout { + use Trait_Translator; + + protected + $_title, + $_rendering, + $_size; + + + public function __construct($title='', $rendering='', $size=9) { + $this->_title = $title; + $this->_rendering = $rendering; + $this->_size = $size; + } + + + public function title() { + return $this->_title; + } + + + public function rendering() { + return $this->_rendering; + } + + + public function size() { + return $this->_size; + } + + + public function newFor($name) { + $def = Intonation_Library_Widget_Carousel_Definition::class; + $map = [$def::WALL => new static($this->_('Mur'), $def::CARD_OVERLAY), + $def::CAROUSEL => new static($this->_('Carousel à une colonnne'), $def::CARD), + $def::MULTIPLE_CAROUSEL => new static($this->_('Carousel à plusieurs colonnes'), + $def::CARD), + $def::LISTING => new static($this->_('Liste verticale'), $def::HORIZONTAL_CARD), + $def::HORIZONTAL_LISTING => new static($this->_('Liste horizontale'), $def::CARD, 5), + $def::LISTING_WITH_OPTIONS => new static($this->_('Liste verticale à interactions'), + $def::HORIZONTAL_CARD) + ]; + + if (array_key_exists($name, $map)) + return $map[$name]; + + return new static(); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Domain/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Domain/Definition.php index 6410ffbd7fc..69fd97a48e6 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Domain/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Domain/Definition.php @@ -38,4 +38,13 @@ class Intonation_Library_Widget_Carousel_Domain_Definition extends Intonation_Li 'order' => Class_CriteresRecherche::SORT_PUBLICATION_DESC, 'root_domain_id' => '']); } + + + protected function _templateLayouts() { + return [static::CAROUSEL, + static::MULTIPLE_CAROUSEL, + static::LISTING, + static::HORIZONTAL_LISTING, + static::WALL]; + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Library/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Library/Definition.php index a5ca1d9fac5..10a0f172566 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Library/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Library/Definition.php @@ -42,4 +42,11 @@ class Intonation_Library_Widget_Carousel_Library_Definition extends Intonation_L 'filters_position' => Class_Systeme_ModulesAccueil_Library::POSITION_RIGHT, 'order' => Class_Systeme_ModulesAccueil_Library::ORDER_ALPHA]); } + + + protected function _templateLayouts() { + return [static::CAROUSEL, + static::MULTIPLE_CAROUSEL, + static::LISTING]; + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Newsletter/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Newsletter/Definition.php index cb3df967994..852f73b2820 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Newsletter/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Newsletter/Definition.php @@ -38,4 +38,12 @@ class Intonation_Library_Widget_Carousel_Newsletter_Definition extends Intonatio ['titre' => $this->_libelle, 'order' => static::SORT_TITLE_ASC]); } + + + protected function _templateLayouts() { + return [static::CAROUSEL, + static::MULTIPLE_CAROUSEL, + static::LISTING, + static::LISTING_WITH_OPTIONS]; + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Record/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Record/Definition.php index e6ef3556cd2..69a6b679876 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Record/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Record/Definition.php @@ -38,4 +38,13 @@ class Intonation_Library_Widget_Carousel_Record_Definition extends Intonation_Li ['titre' => $this->_libelle, 'order' => Class_CriteresRecherche::SORT_RANDOM]); } + + + protected function _templateLayouts() { + return [static::CAROUSEL, + static::MULTIPLE_CAROUSEL, + static::LISTING, + static::HORIZONTAL_LISTING, + static::WALL]; + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Review/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Review/Definition.php index e1e92d8377a..bf974e6cb76 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Review/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Review/Definition.php @@ -44,4 +44,9 @@ class Intonation_Library_Widget_Carousel_Review_Definition extends Intonation_Li ['titre' => $this->_libelle, 'order' => static::SORT_RANDOM]); } + + + protected function _templateLayouts() { + return [static::CAROUSEL, static::MULTIPLE_CAROUSEL]; + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Carousel/Rss/Definition.php b/library/templates/Intonation/Library/Widget/Carousel/Rss/Definition.php index 0f6c2de7f0e..a0ba569d1f0 100644 --- a/library/templates/Intonation/Library/Widget/Carousel/Rss/Definition.php +++ b/library/templates/Intonation/Library/Widget/Carousel/Rss/Definition.php @@ -38,4 +38,12 @@ class Intonation_Library_Widget_Carousel_Rss_Definition extends Intonation_Libra ['titre' => $this->_libelle, 'order' => static::SORT_TITLE_ASC]); } + + + protected function _templateLayouts() { + return [static::CAROUSEL, + static::LISTING, + static::HORIZONTAL_LISTING, + static::LISTING_WITH_OPTIONS]; + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Credits/Definition.php b/library/templates/Intonation/Library/Widget/Credits/Definition.php index 12ca1df4ad2..b268e0f3de4 100644 --- a/library/templates/Intonation/Library/Widget/Credits/Definition.php +++ b/library/templates/Intonation/Library/Widget/Credits/Definition.php @@ -21,6 +21,7 @@ class Intonation_Library_Widget_Credits_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; const CODE = 'CREDITS'; @@ -36,4 +37,9 @@ class Intonation_Library_Widget_Credits_Definition extends Class_Systeme_Modules $this->_view_helper = 'Intonation_Library_Widget_Credits_View'; $this->_defaultValues = ['titre' => $this->_libelle]; } + + + protected function _templateTitle() { + return $this->_('Crédits'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Free/Definition.php b/library/templates/Intonation/Library/Widget/Free/Definition.php index 4a96ccb7e02..5c4270cbb01 100644 --- a/library/templates/Intonation/Library/Widget/Free/Definition.php +++ b/library/templates/Intonation/Library/Widget/Free/Definition.php @@ -21,6 +21,7 @@ class Intonation_Library_Widget_Free_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; const CODE = 'FREE'; @@ -36,4 +37,9 @@ class Intonation_Library_Widget_Free_Definition extends Class_Systeme_ModulesAcc $this->_view_helper = 'Intonation_Library_Widget_Free_View'; $this->_defaultValues = ['titre' => $this->_libelle]; } + + + public function _templateTitle() { + return $this->_('Contenu HTML'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/IdentityProvider/Definition.php b/library/templates/Intonation/Library/Widget/IdentityProvider/Definition.php new file mode 100644 index 00000000000..edb4484c628 --- /dev/null +++ b/library/templates/Intonation/Library/Widget/IdentityProvider/Definition.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +class Intonation_Library_Widget_IdentityProvider_Definition + extends Class_Systeme_ModulesAccueil_IdentityProvider { + use Intonation_Library_Widget_TemplatesAware { + Intonation_Library_Widget_TemplatesAware::templates as traitTemplates; + } + + protected $_group = Class_Systeme_ModulesAccueil::GROUP_ABONNE; + + + public function templates($styles_callback) { + return Class_AdminVar::isIdentityProvidersEnabled() + ? $this->traitTemplates($styles_callback) + : []; + } + + + protected function _templateTitle() { + return $this->_('Se connecter avec'); + } +} diff --git a/library/templates/Intonation/Library/Widget/Image/Definition.php b/library/templates/Intonation/Library/Widget/Image/Definition.php index 1c250cd4e37..65bf3182558 100644 --- a/library/templates/Intonation/Library/Widget/Image/Definition.php +++ b/library/templates/Intonation/Library/Widget/Image/Definition.php @@ -21,6 +21,7 @@ class Intonation_Library_Widget_Image_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; const CODE = 'IMAGE'; @@ -40,4 +41,9 @@ class Intonation_Library_Widget_Image_Definition extends Class_Systeme_ModulesAc 'link' => '', 'link_title' => $this->_('Accéder à %s')]; } + + + protected function _templateTitle() { + return $this->_('Image'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Language/Definition.php b/library/templates/Intonation/Library/Widget/Language/Definition.php index 7f4ea5abda6..021ec50b8d3 100644 --- a/library/templates/Intonation/Library/Widget/Language/Definition.php +++ b/library/templates/Intonation/Library/Widget/Language/Definition.php @@ -21,6 +21,11 @@ class Intonation_Library_Widget_Language_Definition extends Class_Systeme_ModulesAccueil_Langue { + use Intonation_Library_Widget_TemplatesAwareNoHeader; protected $_group = Class_Systeme_ModulesAccueil::GROUP_ABONNE; + + protected function _templateTitle() { + return $this->_('Langues'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Login/Definition.php b/library/templates/Intonation/Library/Widget/Login/Definition.php index b9b0be69c3b..ef5000d7b73 100644 --- a/library/templates/Intonation/Library/Widget/Login/Definition.php +++ b/library/templates/Intonation/Library/Widget/Login/Definition.php @@ -21,6 +21,7 @@ class Intonation_Library_Widget_Login_Definition extends Class_Systeme_ModulesAccueil_Login { + use Intonation_Library_Widget_TemplatesAware; protected $_group = Class_Systeme_ModulesAccueil::GROUP_ABONNE; @@ -43,4 +44,9 @@ class Intonation_Library_Widget_Login_Definition extends Class_Systeme_ModulesAc 'lien_deconnection' => $this->_('Se déconnecter'), Class_Template::current()->withNameSpace('form_style') => 'inline']); } + + + protected function _templateTitle() { + return $this->_('Formulaire de connexion'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Login/View.php b/library/templates/Intonation/Library/Widget/Login/View.php index 4022e2242e6..4a3c0949e1f 100644 --- a/library/templates/Intonation/Library/Widget/Login/View.php +++ b/library/templates/Intonation/Library/Widget/Login/View.php @@ -105,8 +105,7 @@ abstract class IntonationLoginRenderAbstract { 'action' => 'login', 'redirect' => (isset($options['redirect_url'])) ? $options['redirect_url'] - : Class_Url::absolute()] - , null, true); + : Class_Url::absolute()]); $form ->setAction($action); diff --git a/library/templates/Intonation/Library/Widget/Menu/Definition.php b/library/templates/Intonation/Library/Widget/Menu/Definition.php index dc8a4973cd8..6358ec3534c 100644 --- a/library/templates/Intonation/Library/Widget/Menu/Definition.php +++ b/library/templates/Intonation/Library/Widget/Menu/Definition.php @@ -21,6 +21,7 @@ class Intonation_Library_Widget_Menu_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; const CODE = 'MENU', @@ -40,4 +41,9 @@ class Intonation_Library_Widget_Menu_Definition extends Class_Systeme_ModulesAcc 'layout' => static::LAYOUT_HORIZONTAL, 'logo' => '']; } + + + protected function _templateTitle() { + return $this->_('Menu'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Nav/Definition.php b/library/templates/Intonation/Library/Widget/Nav/Definition.php index ad436670fce..d0ec2a6673f 100644 --- a/library/templates/Intonation/Library/Widget/Nav/Definition.php +++ b/library/templates/Intonation/Library/Widget/Nav/Definition.php @@ -21,6 +21,8 @@ class Intonation_Library_Widget_Nav_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; + const CODE = 'NAV'; protected @@ -35,4 +37,9 @@ class Intonation_Library_Widget_Nav_Definition extends Class_Systeme_ModulesAccu 'menu' => '1-H', 'logo' => '']; } + + + protected function _templateTitle() { + return $this->_('Menu principal'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Notify/Definition.php b/library/templates/Intonation/Library/Widget/Notify/Definition.php index 2067a8b112e..c48dbf00a1d 100644 --- a/library/templates/Intonation/Library/Widget/Notify/Definition.php +++ b/library/templates/Intonation/Library/Widget/Notify/Definition.php @@ -21,6 +21,8 @@ class Intonation_Library_Widget_Notify_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; + const CODE = 'NOTIFY'; protected @@ -34,4 +36,9 @@ class Intonation_Library_Widget_Notify_Definition extends Class_Systeme_ModulesA $this->_view_helper = 'Intonation_Library_Widget_Notify_View'; $this->_defaultValues = ['titre' => $this->_libelle]; } + + + protected function _templateTitle() { + return $this->_('Notifications'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Scroll/Definition.php b/library/templates/Intonation/Library/Widget/Scroll/Definition.php index f4e17905713..4a38246ecd8 100644 --- a/library/templates/Intonation/Library/Widget/Scroll/Definition.php +++ b/library/templates/Intonation/Library/Widget/Scroll/Definition.php @@ -21,6 +21,7 @@ class Intonation_Library_Widget_Scroll_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; const CODE = 'SCROLL', @@ -40,4 +41,9 @@ class Intonation_Library_Widget_Scroll_Definition extends Class_Systeme_ModulesA $this->_defaultValues = ['titre' => $this->_libelle, 'direction' => static::UP]; } + + + protected function _templateTitle() { + return $this->_('Défiler vers le haut'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Search/Definition.php b/library/templates/Intonation/Library/Widget/Search/Definition.php index cfa1134fb62..c500bdd5ba7 100644 --- a/library/templates/Intonation/Library/Widget/Search/Definition.php +++ b/library/templates/Intonation/Library/Widget/Search/Definition.php @@ -20,7 +20,10 @@ */ -class Intonation_Library_Widget_Search_Definition extends Class_Systeme_ModulesAccueil_RechercheSimple { +class Intonation_Library_Widget_Search_Definition + extends Class_Systeme_ModulesAccueil_RechercheSimple { + use Intonation_Library_Widget_TemplatesAware; + public function __construct() { parent::__construct(); $this->_view_helper = 'Intonation_Library_Widget_Search_View'; @@ -38,4 +41,9 @@ class Intonation_Library_Widget_Search_Definition extends Class_Systeme_ModulesA 'always_new_search' => 0 ]); } + + + protected function _templateTitle() { + return $this->_('Rechercher'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/Share/Definition.php b/library/templates/Intonation/Library/Widget/Share/Definition.php index 19e8973053b..08370ed311d 100644 --- a/library/templates/Intonation/Library/Widget/Share/Definition.php +++ b/library/templates/Intonation/Library/Widget/Share/Definition.php @@ -21,6 +21,7 @@ class Intonation_Library_Widget_Share_Definition extends Class_Systeme_ModulesAccueil_Null { + use Intonation_Library_Widget_TemplatesAwareNoHeader; const CODE = 'SHARE'; @@ -36,4 +37,9 @@ class Intonation_Library_Widget_Share_Definition extends Class_Systeme_ModulesAc $this->_view_helper = 'Intonation_Library_Widget_Share_View'; $this->_defaultValues = ['titre' => $this->_libelle]; } + + + protected function _templateTitle() { + return $this->_('Partager la page'); + } } \ No newline at end of file diff --git a/library/templates/Intonation/Library/Widget/TemplatesAware.php b/library/templates/Intonation/Library/Widget/TemplatesAware.php new file mode 100644 index 00000000000..16ee6ce1208 --- /dev/null +++ b/library/templates/Intonation/Library/Widget/TemplatesAware.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +trait Intonation_Library_Widget_TemplatesAware { + public function templates($styles_callback) { + return [['title' => $this->_templateTitle(), + 'icon' => 'TEMPLATE_' . static::CODE . '.jpg', + 'type' => static::CODE, + 'configuration' => $this->_templateConfiguration($styles_callback)]]; + } + + + protected function _templateConfiguration($styles_callback) { + return ['boite' => $styles_callback()]; + } +} diff --git a/library/templates/Intonation/Library/Widget/TemplatesAwareNoHeader.php b/library/templates/Intonation/Library/Widget/TemplatesAwareNoHeader.php new file mode 100644 index 00000000000..4b08a781c5d --- /dev/null +++ b/library/templates/Intonation/Library/Widget/TemplatesAwareNoHeader.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +trait Intonation_Library_Widget_TemplatesAwareNoHeader { + use Intonation_Library_Widget_TemplatesAware { + Intonation_Library_Widget_TemplatesAware::_templateConfiguration as _parentTemplateConfiguration; + } + + protected function _templateConfiguration($styles_callback) { + $conf = $this->_parentTemplateConfiguration($styles_callback); + $show_header_key = Class_Template::current()->withNameSpace('show_header'); + $conf[$show_header_key] = 0; + + return $conf; + } +} diff --git a/library/templates/Intonation/Library/WidgetTemplates.php b/library/templates/Intonation/Library/WidgetTemplates.php index 3abf83c87db..1c4db1744ea 100644 --- a/library/templates/Intonation/Library/WidgetTemplates.php +++ b/library/templates/Intonation/Library/WidgetTemplates.php @@ -21,175 +21,32 @@ class Intonation_Library_WidgetTemplates { - use Trait_Translator; - public function getTemplates() { - $sections = - [$this->_('Agenda') => ['CALENDAR' => ['carousel', - 'multiple_carousel', - 'list', - 'wall']], - - $this->_('Articles') => ['NEWS' => ['carousel', - 'multiple_carousel', - 'list', - 'horizontal_list', - 'wall']], - - $this->_('Auteurs') => ['AUTHORS' => ['carousel', - 'multiple_carousel', - 'horizontal_list', - 'wall']], - - $this->_('Avis') => ['CRITIQUES' => ['carousel', - 'multiple_carousel']], - - $this->_('Bibliothèques') => ['LIBRARY' => ['carousel', - 'multiple_carousel', - 'list']], - - $this->_('Connexion') => ['LOGIN' => ['' => ['title' => $this->_('Formulaire de connexion')]]], - - $this->_('Domaines') => ['DOMAIN_BROWSER' => ['carousel', - 'multiple_carousel', - 'list', - 'horizontal_list', - 'wall']], - - $this->_('Flux RSS') => ['RSS' => ['carousel', - 'list', - 'horizontal_list', - 'list_with_options']], - - $this->_('Lettres d\'informations') => ['NEWSLETTERS' => ['carousel', - 'multiple_carousel', - 'list', - 'list_with_options']], - - $this->_('Navigation et menus') => ['ARIANE' => ['' => ['title' => $this->_('Fil d\'Ariane'), - Class_Template::current()->withNameSpace('show_header') => 0]], - 'MENU' => ['' => ['title' => $this->_('Menu'), - Class_Template::current()->withNameSpace('show_header') => 0]], - 'NAV' => ['' => ['title' => $this->_('Menu principal'), - Class_Template::current()->withNameSpace('show_header') => 0]]], - - $this->_('Notices') => ['KIOSQUE' => ['carousel', - 'multiple_carousel', - 'list', - 'horizontal_list', - 'wall']], - - $this->_('Site') => ['ACCESSIBILITY' => ['' => ['title' => $this->_('Accessibilité'), - Class_Template::current()->withNameSpace('show_header') => 0]], - 'FREE' => ['' => ['title' => $this->_('Contenu HTML'), - Class_Template::current()->withNameSpace('show_header') => 0]], - 'CREDITS' => ['' => ['title' => $this->_('Crédits'), - Class_Template::current()->withNameSpace('show_header') => 0]], - 'SCROLL' => ['' => ['title' => $this->_('Défiler vers le haut'), - Class_Template::current()->withNameSpace('show_header') => 0]], - 'IMAGE' => ['' => ['title' => $this->_('Image'), - Class_Template::current()->withNameSpace('show_header') => 0]], - 'LANGUE' => ['' => ['title' => $this->_('Langues'), - Class_Template::current()->withNameSpace('show_header') => 0]], - 'NOTIFY' => ['' => ['title' => $this->_('Notifications')]], - 'ADMIN_TOOLS' => ['' => ['title' => $this->_('Outils d\'administration'), - Class_Template::current()->withNameSpace('show_header') => 0]], - 'SHARE' => ['' => ['title' => $this->_('Partager la page'), - Class_Template::current()->withNameSpace('show_header') => 0]]], - - $this->_('Recherche') => ['RECH_SIMPLE' => ['' => ['title' => $this->_('Rechercher')]]] - ]; - - $sections_content = []; - foreach ($sections as $title => $settings) - $sections_content [] = ['title' => $title, - 'templates' => $this->_createTemplatesFrom($settings)]; - - return ['sections' => $sections_content]; - } - - - protected function _createTemplatesFrom($settings) { - $templates = []; - foreach ($settings as $type => $layouts) - $templates = $this->_createTemplateFor($type, $layouts, $templates); - - return $templates; - } - - - protected function _createTemplateFor($type, $layouts, $templates) { - foreach($layouts as $layout) - $templates = $this->_createWidgetFor($type, $layout, $templates); - - return $templates; - } - - - protected function _createWidgetFor($type, $layout, $templates) { - $title = ''; - $rendering = ''; - $size = 9; - - if (is_array($layout)) { - $title = $layout['title']; - - $boite = ((($type == 'NAV') - || ($type == 'MENU')) - ? $this->_getMenuDefaultStyles() - : $this->_getDefaultStyles()); - - unset($layout['title']); - $templates [] = ['title' => $title, - 'icon' => 'TEMPLATE_' . $type . '.jpg', - 'type' => $type, - 'configuration' => array_merge($layout, - ['boite' => $boite])]; - return $templates; - } - - if ('wall' == $layout) { - $title = $this->_('Mur'); - $rendering = 'card-overlay'; - } - - if ('list' == $layout) { - $title = $this->_('Liste verticale'); - $rendering = 'card-horizontal'; - } - - if ('list_with_options' == $layout) { - $title = $this->_('Liste verticale à interactions'); - $rendering = 'card-horizontal'; - } - - if ('horizontal_list' == $layout) { - $title = $this->_('Liste horizontale'); - $rendering = 'card'; - $size = 5; - } - - if ('carousel' == $layout) { - $title = $this->_('Carousel à une colonnne'); - $rendering = 'card'; - } - - if ('multiple_carousel' == $layout) { - $title = $this->_('Carousel à plusieurs colonnes'); - $rendering = 'card'; - } + $sections_content = + (new Storm_Collection([new Intonation_Library_WidgetTemplates_Agenda($this), + new Intonation_Library_WidgetTemplates_News($this), + new Intonation_Library_WidgetTemplates_Author($this), + new Intonation_Library_WidgetTemplates_Review($this), + new Intonation_Library_WidgetTemplates_Library($this), + new Intonation_Library_WidgetTemplates_Login($this), + new Intonation_Library_WidgetTemplates_Domain($this), + new Intonation_Library_WidgetTemplates_Rss($this), + new Intonation_Library_WidgetTemplates_Newsletter($this), + new Intonation_Library_WidgetTemplates_Nav($this), + new Intonation_Library_WidgetTemplates_Record($this), + new Intonation_Library_WidgetTemplates_Site($this), + new Intonation_Library_WidgetTemplates_Search($this), + ])) + ->collect(function($section) { return $section->definition(); }); + + return ['sections' => (array)$sections_content]; + } - $templates [] = ['title' => $title, - 'icon' => 'TEMPLATE_' . $type . '_' . $layout . '.jpg', - 'type' => $type, - 'configuration' => ['rendering' => $rendering, - 'layout' => $layout, - 'size' => $size, - 'boite' => $this->_getDefaultStyles()]]; - return $templates; + public function defaultStyles() { + return $this->_getDefaultStyles(); } @@ -203,6 +60,11 @@ class Intonation_Library_WidgetTemplates { } + public function defaultMenuStyles() { + return $this->_getMenuDefaultStyles(); + } + + protected function _getMenuDefaultStyles() { return ['no_background', 'no_border', @@ -211,3 +73,286 @@ class Intonation_Library_WidgetTemplates { 'black_and_white']; } } + + + + +class Intonation_Library_WidgetTemplates_Section { + use Trait_Translator; + + protected + $_widgets, + $_all; + + public function __construct($all) { + $this->_widgets = new Storm_Collection; + $this->_all = $all; + $this->_init(); + } + + + protected function _init() { + } + + + public function definition() { + return ['title' => $this->_title(), + 'templates' => $this->_widgetsDefinition()]; + } + + + protected function _widgetsDefinition() { + return $this->_widgets->injectInto([], [$this, 'widgetDefinition']); + } + + + public function widgetDefinition($value, $widget) { + $class_name = get_class($widget); + $code = $class_name::CODE; + foreach($widget->templates($this->_defaultStyles($code)) as $template) + $value[] = $template; + + return $value; + } + + + protected function _defaultStyles($code) { + return [$this->_all, 'defaultStyles']; + } + + + protected function _title() { + return ''; + } +} + + + + +class Intonation_Library_WidgetTemplates_Agenda + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Carousel_Agenda_Definition); + } + + + protected function _title() { + return $this->_('Agenda'); + } +} + + + + +class Intonation_Library_WidgetTemplates_News + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Carousel_Article_Definition); + } + + + protected function _title() { + return $this->_('Articles'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Author + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Carousel_Author_Definition); + } + + + protected function _title() { + return $this->_('Auteurs'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Review + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Carousel_Review_Definition); + } + + + protected function _title() { + return $this->_('Avis'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Library + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Carousel_Library_Definition); + } + + + protected function _title() { + return $this->_('Bibliothèques'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Login + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->addAll([new Intonation_Library_Widget_Login_Definition, + new Intonation_Library_Widget_IdentityProvider_Definition]); + } + + + protected function _title() { + return $this->_('Connexion'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Domain + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Carousel_Domain_Definition); + } + + + protected function _title() { + return $this->_('Domaines'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Rss + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Carousel_Rss_Definition); + } + + + protected function _title() { + return $this->_('Flux RSS'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Newsletter + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Carousel_Newsletter_Definition); + } + + + protected function _title() { + return $this->_('Lettres d\'informations'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Nav + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->addAll([new Intonation_Library_Widget_Breadcrumb_Definition, + new Intonation_Library_Widget_Menu_Definition, + new Intonation_Library_Widget_Nav_Definition, + ]); + } + + + protected function _title() { + return $this->_('Navigation et menus'); + } + + + protected function _defaultStyles($code) { + $method = in_array($code, [Intonation_Library_Widget_Menu_Definition::CODE, + Intonation_Library_Widget_Nav_Definition::CODE]) + ? 'defaultMenuStyles' + : 'defaultStyles'; + + return [$this->_all, $method]; + } +} + + + + +class Intonation_Library_WidgetTemplates_Record + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Carousel_Record_Definition); + } + + + protected function _title() { + return $this->_('Notices'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Site + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->addAll([new Intonation_Library_Widget_Accessibility_Definition, + new Intonation_Library_Widget_Free_Definition, + new Intonation_Library_Widget_Credits_Definition, + new Intonation_Library_Widget_Scroll_Definition, + new Intonation_Library_Widget_Image_Definition, + new Intonation_Library_Widget_Language_Definition, + new Intonation_Library_Widget_Notify_Definition, + new Intonation_Library_Widget_AdminTools_Definition, + new Intonation_Library_Widget_Share_Definition, + ]); + } + + + protected function _title() { + return $this->_('Site'); + } +} + + + + +class Intonation_Library_WidgetTemplates_Search + extends Intonation_Library_WidgetTemplates_Section { + + public function _init() { + $this->_widgets->append(new Intonation_Library_Widget_Search_Definition); + } + + + protected function _title() { + return $this->_('Recherche'); + } +} diff --git a/library/templates/Intonation/Template.php b/library/templates/Intonation/Template.php index 8be15ce3888..0f4e67b1ae8 100644 --- a/library/templates/Intonation/Template.php +++ b/library/templates/Intonation/Template.php @@ -183,7 +183,9 @@ class Intonation_Template extends Class_Template { Intonation_Library_Widget_Menu_Definition::CODE => new Intonation_Library_Widget_Menu_Definition, - Intonation_Library_Widget_Carousel_Rss_Definition::CODE => new Intonation_Library_Widget_Carousel_Rss_Definition + Intonation_Library_Widget_Carousel_Rss_Definition::CODE => new Intonation_Library_Widget_Carousel_Rss_Definition, + + Intonation_Library_Widget_IdentityProvider_Definition::CODE => new Intonation_Library_Widget_IdentityProvider_Definition ]; } diff --git a/library/templates/Intonation/View/IdentityProviders.php b/library/templates/Intonation/View/IdentityProviders.php new file mode 100644 index 00000000000..fe964b0da99 --- /dev/null +++ b/library/templates/Intonation/View/IdentityProviders.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +class Intonation_View_IdentityProviders extends ZendAfi_View_Helper_IdentityProviders { + protected function _renderButton($provider) { + return $this->_div(['class' => 'justify-content-center row no-gutters m-2'], + parent::_renderButton($provider)); + } +} diff --git a/library/templates/Intonation/View/RenderLogin.php b/library/templates/Intonation/View/RenderLogin.php new file mode 100644 index 00000000000..f53196e79f2 --- /dev/null +++ b/library/templates/Intonation/View/RenderLogin.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +class Intonation_View_RenderLogin extends ZendAfi_View_Helper_BaseHelper { + + public function renderLogin($settings, $title, $message) { + return $this->_div(['class' => 'justify-content-center row no-gutters'], + $this->_renderTitle($title) + . $this->_renderMessage($message) + . $this->view->widget_Login($settings)); + } + + + protected function _renderTitle($title) { + return $this->_tag('h2', + Class_Template::current()->getIco($this->view, 'login', 'utils') + . ' ' . $title, + ['class' => 'col-md-6 col-sm-12']); + } + + + protected function _renderMessage($message) { + if (!$message) + return ''; + + return $this->_div(['class' => 'container-fluid'], + $this->_div(['class' => 'justify-content-center row'], + $this->_div(['class' => 'col-md-6 col-sm-12'], $message))); + } +} diff --git a/library/templates/Intonation/View/Widget/Login.php b/library/templates/Intonation/View/Widget/Login.php index d80b4e79e5f..a42e889c663 100644 --- a/library/templates/Intonation/View/Widget/Login.php +++ b/library/templates/Intonation/View/Widget/Login.php @@ -32,6 +32,7 @@ class Intonation_View_Widget_Login extends ZendAfi_View_Helper_BaseHelper { :[]))) ->setView($this->view); $login->getHtml(); + return $login->getContent(); } } \ No newline at end of file diff --git a/public/opac/js/widget_templates/TEMPLATE_IDENTITY_PROVIDER.jpg b/public/opac/js/widget_templates/TEMPLATE_IDENTITY_PROVIDER.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2165b710aeed7186e2632edc2d4335cbbdedb323 GIT binary patch literal 22202 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 literal 0 HcmV?d00001 diff --git a/public/opac/js/widget_templates/TEMPLATE_LANGUE.jpg b/public/opac/js/widget_templates/TEMPLATE_LANGUE.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b492dd1efc672416c82b35d63a5346c2df01cdf8 GIT binary patch literal 11804 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 K&HpOTOaBYOIQ1$3 literal 0 HcmV?d00001 diff --git a/tests/application/modules/AbstractControllerTestCase.php b/tests/application/modules/AbstractControllerTestCase.php index 8993a0aa427..ce353945d85 100644 --- a/tests/application/modules/AbstractControllerTestCase.php +++ b/tests/application/modules/AbstractControllerTestCase.php @@ -152,6 +152,7 @@ abstract class AbstractControllerTestCase extends Zend_Test_PHPUnit_ControllerTe ->willDo(function($pass, $crypt) { return $pass; })); Class_WebService_BibNumerique_RessourceNumerique::setCommand($this->mock()->whenCalled('execTimedScript')->answers('')); Class_Notice_Thumbnail_ProviderCacheServer::doNotTrackSeen(); + Class_CriteresRecherche::setMaxSearchResults(''); } diff --git a/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php b/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php index ab30f8c7943..9a4dc956f3d 100644 --- a/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php +++ b/tests/scenarios/DriveCheckOut/DriveCheckOutBookingTest.php @@ -237,7 +237,7 @@ class DriveCheckOutUserNotificationsTest extends DriveCheckOutBookingTestCase { 'controller' => 'drive-checkout', 'action' => 'plan']); $this->assertEquals([$url => 'Planifier le retrait de mes documents'], - $this->_actions[0]); + $this->_actions[0]); } @@ -957,7 +957,7 @@ class DriveCheckOutBookingPlanNotLoggedTest extends DriveCheckOutBookingTestCase /** @test */ public function pageShouldContainLoginForm() { - $this->assertXPath('//div[@class="contenu"]//form[contains(@action, "/auth/login")]'); + $this->assertXPath('//div[@data-action="drive-checkout_plan"]//form[contains(@action, "/auth/login")]'); } } diff --git a/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php b/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php index 9c183714ead..6d1bbefedc0 100644 --- a/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php +++ b/tests/scenarios/IdentityProvider/IdentityProviderAdminTest.php @@ -125,6 +125,11 @@ class IdentityProviderAdminAddTest extends IdentityProviderAdminTestCase { } + /** @test */ + public function typeSelectShouldContainCasOption() { + $this->assertXPath('//select[@name="type"]/option[@value="cas2"]'); + } + /** @test */ public function formShouldContainsInputForClientId() { $this->assertXPath('//input[@name="client_id"]'); @@ -141,6 +146,12 @@ class IdentityProviderAdminAddTest extends IdentityProviderAdminTestCase { public function formShouldContainsUrlInput() { $this->assertXPath('//input[@name="url"]'); } + + + /** @test */ + public function formShouldContainsAssociateOnLogin() { + $this->assertXPath('//input[@name="associate_on_login"]'); + } } @@ -249,6 +260,66 @@ class IdentityProviderAdminPostAddTest extends IdentityProviderAdminTestCase { +class IdentityProviderAdminPostAddInvalidGoogleTest extends IdentityProviderAdminTestCase { + protected $_provider; + + public function setUp() { + parent::setUp(); + + $this->postDispatch('/admin/identity-providers/add', + ['label' => 'AFI Identities', + 'url' => 'https://www.afi-identities.fr', + 'type' => 'google']); + + $this->_provider = Class_IdentityProvider::find(1); + } + + + /** @test */ + public function providerShouldBeNull() { + $this->assertNull($this->_provider); + } + + + /** @test */ + public function clientIdShouldBeMantory() { + $this->assertXPathContentContains('//ul[@class="errors"]//li', 'Identifiant client est obligatoire'); + } + + + /** @test */ + public function clientSecretShouldBeMantory() { + $this->assertXPathContentContains('//ul[@class="errors"]//li', 'Clé secrète est obligatoire'); + } +} + + + + +class IdentityProviderAdminPostAddCasTest extends IdentityProviderAdminTestCase { + protected $_provider; + + public function setUp() { + parent::setUp(); + + $this->postDispatch('/admin/identity-providers/add', + ['label' => 'AFI Identities', + 'url' => 'https://www.afi-identities.fr', + 'type' => 'cas2']); + + $this->_provider = Class_IdentityProvider::find(1); + } + + + /** @test */ + public function providerShouldBeNotnull() { + $this->assertNotNull($this->_provider); + } +} + + + + class IdentityProviderAdminWidgetTest extends Admin_AbstractControllerTestCase { protected $_storm_default_to_volatile = true; diff --git a/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationCasTest.php b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationCasTest.php new file mode 100644 index 00000000000..4088f3588cb --- /dev/null +++ b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationCasTest.php @@ -0,0 +1,585 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +require_once 'tests/fixtures/NanookFixtures.php'; + + +abstract class IdentityProviderAuthenticationCasTestCase extends AbstractControllerTestCase { + protected + $_storm_default_to_volatile = true, + $_provider; + + + public function setUp() { + parent::setUp(); + Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1); + + $this->_provider = $this->fixture('Class_IdentityProvider', + ['id' => 1, + 'label' => 'CAS 2.0', + 'type' => 'cas2', + 'url' => 'http://moncompte.server.com/', + ]); + + } + + + public function tearDown() { + Class_WebService_Cas2::setWebClient(null); + Class_WebService_Cas2::clearSession(); + parent::tearDown(); + } +} + + + + +class IdentityProviderAuthenticationCasRedirectToLoginTest + extends IdentityProviderAuthenticationCasTestCase { + + /** @test */ + public function shouldRedirectToCasServerLogin() { + $this->dispatch('/identity-providers/authenticate/id/1'); + $this->assertRedirectContains('http://moncompte.server.com/login?service='); + } +} + + + + +class IdentityProviderAuthenticationCasInvalidTicketNotLoggedNotAssociatedTest + extends IdentityProviderAuthenticationCasTestCase { + + public function setUp() { + parent::setUp(); + ZendAfi_Auth::getInstance()->clearIdentity(); + Class_WebService_Cas2::loginWith('mysuperid'); + + $response = $this->mock() + ->whenCalled('isError')->answers(false) + + ->whenCalled('getBody') + ->answers('<?xml version="1.0" encoding="UTF-8" ?> +<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> + <cas:authenticationFailure code="INVALID_TICKET"> + Ticket testticket not recognized + </cas:authenticationFailure> +</cas:serviceResponse>'); + + Class_WebService_Cas2::setWebClient($this->mock() + ->whenCalled('getResponse') + ->answers($response)); + + $this->dispatch('/auth/login/provider/1?ticket=testticket'); + } + + + /** @test */ + public function shouldClearRemotelyLogged() { + $this->assertFalse($this->_provider->isRemotelyLogged()); + } +} + + + + +class IdentityProviderAuthenticationCasValidTicketNotLoggedNotAssociatedTest + extends IdentityProviderAuthenticationCasTestCase { + + public function setUp() { + parent::setUp(); + ZendAfi_Auth::getInstance()->clearIdentity(); + + $response = $this->mock() + ->whenCalled('isError')->answers(false) + + ->whenCalled('getBody') + ->answers('<?xml version="1.0" encoding="UTF-8" ?> +<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> + <cas:authenticationSuccess> + <cas:user>mysuperid</cas:user> + <cas:proxyGrantingTicket>nimportenawak</cas:proxyGrantingTicket> + </cas:authenticationSuccess> +</cas:serviceResponse>'); + + Class_WebService_Cas2::setWebClient($this->mock() + ->whenCalled('getResponse') + ->answers($response)); + + $this->dispatch('/auth/login/provider/1?ticket=testticket'); + } + + + /** @test */ + public function shouldNotValidateTicketTwice() { + $this->assertEquals(1, Class_WebService_Cas2::getWebClient()->methodCallCount('getResponse')); + } + + + /** @test */ + public function shouldDisplayLoginForm() { + $this->assertXPath('//input[@name="username"]'); + } + + + /** @test */ + public function shouldBecomeRemotlyLoggedWithCasUserAsId() { + $this->assertTrue($this->_provider->isRemotelyLogged()); + $this->assertEquals('mysuperid', $this->_provider->getRemoteUserId()); + } + + + /** @test */ + public function pageTitleShouldContainsBeAssocierVotreCompte() { + $this->assertXPathContentContains('//title', 'Associer votre compte'); + } + + + /** @test */ + public function pageShouldDisplayConnectedToCas2_0() { + $this->assertXPathContentContains('//p', 'Vous êtes connecté à CAS 2.0'); + } + + + /** @test */ + public function logoutBokehShouldLogoutCas2_0() { + $this->dispatch('/auth/logout'); + $this->assertRedirect('/'); + } +} + + + + +class IdentityProviderAuthenticationCasAuthLoginPostRemotelyLoggedTest + extends IdentityProviderAuthenticationCasTestCase { + + public function setUp() { + parent::setUp(); + + ZendAfi_Auth::getInstance()->clearIdentity(); + Class_WebService_Cas2::loginWith('mysuperid'); + NanookFixtures::connectNanook($this, 'name@server.tld', '1987'); + + $this->postDispatch('/auth/login/provider/1', + ['username' => 'name@server.tld', + 'password' => '1987']); + } + + + /** @test */ + public function identiyShouldBeCreated() { + $this->assertNotNull(Class_User_Identity::findFirstBy(['provider_id' => 1, + 'identifier' => 'mysuperid'])); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirect(); + } + + + /** @test */ + public function shouldNotifyAssociationComplete() { + $this->assertFlashMessengerContentContains('L\'association de votre compte à CAS 2.0 a réussi'); + } +} + + + + +class IdentityProviderAuthenticationCasAuthLoginIdentityAssociatedToAnotherTest + extends IdentityProviderAuthenticationCasTestCase { + + public function setUp() { + parent::setUp(); + + Class_WebService_Cas2::loginWith('mysuperid'); + + $this->fixture('Class_User_Identity', + ['id' => 803, + 'provider_id' => 1, + 'user_id' => 999, + 'identifier' => 'mysuperid']); + + $response = $this->mock() + ->whenCalled('isError')->answers(false) + + ->whenCalled('getBody') + ->answers('<?xml version="1.0" encoding="UTF-8" ?> +<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> + <cas:authenticationSuccess> + <cas:user>mysuperid</cas:user> + <cas:proxyGrantingTicket>nimportenawak</cas:proxyGrantingTicket> + </cas:authenticationSuccess> +</cas:serviceResponse>'); + + Class_WebService_Cas2::setWebClient($this->mock() + ->whenCalled('getResponse') + ->answers($response)); + + $this->dispatch('/auth/login/provider/1?ticket=testticket'); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirect(); + } + + + /** @test */ + public function shouldNotDuplicate() { + $this->assertEquals(1, Class_User_Identity::countBy(['identifier' => 'mysuperid', + 'provider_id' => 1])); + } + + + /** @test */ + public function shouldNotifyAssociationNotComplete() { + $this->assertFlashMessengerContentContains('L\'association a échoué, cette identité est déjà associée à un autre compte'); + } +} + + + +abstract class IdentityProviderAuthenticationCasAutoAssociationTestCase + extends IdentityProviderAuthenticationCasTestCase { + + public function setUp() { + parent::setUp(); + + ZendAfi_Auth::getInstance()->clearIdentity(); + + $this->_provider->setAssociateOnLogin(1); + + $response = $this->mock() + ->whenCalled('isError')->answers(false) + + ->whenCalled('getBody') + ->answers('<?xml version="1.0" encoding="UTF-8" ?> +<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> + <cas:authenticationSuccess> + <cas:user>mysuperid</cas:user> + <cas:proxyGrantingTicket>nimportenawak</cas:proxyGrantingTicket> + </cas:authenticationSuccess> +</cas:serviceResponse>'); + + Class_WebService_Cas2::setWebClient($this->mock() + ->whenCalled('getResponse') + ->answers($response)); + } +} + + + + +class IdentityProviderAuthenticationCasAuthLoginPostAutoAssociationTest + extends IdentityProviderAuthenticationCasAutoAssociationTestCase { + + public function setUp() { + parent::setUp(); + + $this->fixture('Class_Users', ['id' => 999, + 'login' => 'mysuperid', + 'password' => 'infopassword']); + + $this->dispatch('/auth/login/provider/1?ticket=testticket'); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirect(); + } + + + /** @test */ + public function shouldAssociateOnLogin() { + $this->assertNotNull(Class_User_Identity::findFirstBy(['provider_id' => 1, + 'user_id' => 999, + 'identifier' => 'mysuperid'])); + } + + + /** @test */ + public function shouldBeConnected() { + $this->assertNotNull(Class_Users::getIdentity()); + } +} + + + + +class IdentityProviderAuthenticationCasAuthLoginPostAutoAssociationWithDuplicateTest + extends IdentityProviderAuthenticationCasAutoAssociationTestCase { + + public function setUp() { + parent::setUp(); + + $this->fixture('Class_Users', ['id' => 999, + 'login' => 'mysuperid', + 'password' => 'infopassword']); + + $this->fixture('Class_Users', ['id' => 1000, + 'login' => 'mysuperid', + 'password' => 'infopassword']); + + $this->dispatch('/auth/login/provider/1?ticket=testticket'); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirect(); + } + + + /** @test */ + public function shouldNotAssociateOnLogin() { + $this->assertNull(Class_User_Identity::findFirstBy(['provider_id' => 1, + 'user_id' => 999, + 'identifier' => 'mysuperid'])); + } + + + /** @test */ + public function shouldNotifyDuplicateLogin() { + $this->assertFlashMessengerContentContains('Connection impossible, l\'identifiant est dupliqué'); + } + + + /** @test */ + public function shouldNotBeConnected() { + $this->assertNull(Class_Users::getIdentity()); + } +} + + + + +class IdentityProviderAuthenticationCasAuthLoginPostAutoAssociationUnknownLoginTest + extends IdentityProviderAuthenticationCasAutoAssociationTestCase { + + public function setUp() { + parent::setUp(); + $this->dispatch('/auth/login/provider/1?ticket=testticket'); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirect(); + } + + + /** @test */ + public function shouldNotAssociateOnLogin() { + $this->assertNull(Class_User_Identity::findFirstBy(['provider_id' => 1, + 'user_id' => 999, + 'identifier' => 'mysuperid'])); + } + + + /** @test */ + public function shouldNotifyDuplicateLogin() { + $this->assertFlashMessengerContentContains('Connection impossible, l\'identifiant n\'existe pas'); + } + + + /** @test */ + public function shouldNotBeConnected() { + $this->assertNull(Class_Users::getIdentity()); + } +} + + + + +class IdentityProviderAuthenticationCasAuthLoginPostNotRemotelyLoggedTest + extends IdentityProviderAuthenticationCasTestCase { + + public function setUp() { + parent::setUp(); + + ZendAfi_Auth::getInstance()->clearIdentity(); + NanookFixtures::connectNanook($this, 'name@server.tld', '1987'); + + $this->postDispatch('/auth/login/provider/1', + ['username' => 'name@server.tld', + 'password' => '1987']); + } + + + /** @test */ + public function identiyShouldNotBeCreated() { + $this->assertNull(Class_User_Identity::findFirstBy(['provider_id' => 1])); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirect(); + } + + + /** @test */ + public function shouldNotifyAssociationComplete() { + $this->assertFlashMessengerContentContains('L\'association de votre compte à CAS 2.0 a échoué'); + } +} + + + + +class IdentityProviderAuthenticationCasAuthLoginRemotelyLoggedAlreadyAssociatedTest + extends IdentityProviderAuthenticationCasTestCase { + + public function setUp() { + parent::setUp(); + + ZendAfi_Auth::getInstance()->clearIdentity(); + + $this->fixture('Class_Users', + ['id' => 33, + 'login' => 'name@server.tld', + 'password' => 'supersecret', + ]); + + $this->fixture('Class_User_Identity', + ['id' => 1, + 'provider_id' => 1, + 'user_id' => 33, + 'identifier' => 'mysuperid']); + + $response = $this->mock() + ->whenCalled('isError')->answers(false) + + ->whenCalled('getBody') + ->answers('<?xml version="1.0" encoding="UTF-8" ?> +<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> + <cas:authenticationSuccess> + <cas:user>mysuperid</cas:user> + <cas:proxyGrantingTicket>teststicket</cas:proxyGrantingTicket> + </cas:authenticationSuccess> +</cas:serviceResponse>'); + + Class_WebService_Cas2::setWebClient($this->mock() + ->whenCalled('getResponse') + ->answers($response)); + + $this->dispatch('/auth/login/provider/1?ticket=testticket'); + } + + + /** @test */ + public function userShouldBeLoggedIn() { + $this->assertEquals('name@server.tld', Class_Users::getIdentity()->getLogin()); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirect(); + } +} + + + + +class IdentityProviderAuthenticationCasAuthLogoutRemotelyLoggedAlreadyAssociatedTest + extends IdentityProviderAuthenticationCasTestCase { + + public function setUp() { + parent::setUp(); + + $user = $this->fixture('Class_Users', + ['id' => 33, + 'login' => 'name@server.tld', + 'password' => 'supersecret', + ]); + + ZendAfi_Auth::getInstance()->logUser($user); + + $this->fixture('Class_User_Identity', + ['id' => 1, + 'provider_id' => 1, + 'user_id' => 33, + 'identifier' => 'mysuperid']); + + Class_WebService_Cas2::loginWith('mysuperid'); + Class_WebService_Cas2::setWebClient($this->mock()); + + $this->dispatch('/auth/logout/provider/1'); + } + + + /** @test */ + public function shouldBeLoggedOut() { + $this->assertNull(Class_Users::getIdentity()); + } + + + /** @test */ + public function shouldBeNoLongerRemotelyLogged() { + $this->assertFalse($this->_provider->isRemotelyLogged()); + } + + + /** @test */ + public function shouldRedirectToCasLogout() { + $this->assertRedirectContains('http://moncompte.server.com/logout?service=http'); + } +} + + + + +class IdentityProviderAuthenticationCasNetworkErrorTest + extends IdentityProviderAuthenticationCasTestCase { + + public function setUp() { + parent::setUp(); + ZendAfi_Auth::getInstance()->clearIdentity(); + + Class_WebService_Cas2::setWebClient($this->mock() + ->whenCalled('getResponse') + ->willDo(function() + { + throw new RuntimeException("Erreur Réseau"); + })); + + $this->dispatch('/auth/login/provider/1?ticket=testticket'); + } + + + /** @test */ + public function shouldDisplayLoginForm() { + $this->assertXPath('//input[@name="username"]'); + } + + + /** @test */ + public function shouldNotBecomeRemotlyLogged() { + $this->assertFalse($this->_provider->isRemotelyLogged()); + } +} \ No newline at end of file diff --git a/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php index c620f99960a..8c7efe11ffd 100644 --- a/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php +++ b/tests/scenarios/IdentityProvider/IdentityProviderAuthenticationTest.php @@ -115,6 +115,13 @@ class IdentityProviderAuthenticationBoxDisplayTest extends AbstractControllerTes 'client_secret' => '9876']) ]); + $this->fixture('Class_IdentityProvider', + ['id' => 6, + 'label' => 'CAS 2.0', + 'type' => 'cas2', + 'config' => json_encode(['url' => 'https://moncas.server.com']) + ]); + Class_WebService_OpenId::setWebClient($this->mock()->whenCalled('open_url')->answers('')); $this->dispatch('/'); @@ -154,6 +161,13 @@ class IdentityProviderAuthenticationBoxDisplayTest extends AbstractControllerTes } + /** @test */ + public function pageShouldContainsButtonForCas() { + $this->assertXPath('//button[contains(@onclick, "/identity-providers/authenticate/id/6")]', + $this->_response->getBody()); + } + + /** @test */ public function pageShouldContainsButtonOpenIdConnectForBiblibre() { $this->assertXPathContentContains('//button[contains(@onclick, "/identity-providers/authenticate/id/2")]', @@ -539,7 +553,9 @@ class IdentityProviderAuthenticationCallbackAuthenticationFirstTime $this->fixture('Class_IdentityProvider', ['id' => 12, 'label' => 'FranceConnect', - 'type' => 'franceconnect']); + 'type' => 'franceconnect', + 'client_id' => 'myclientid', + 'client_secret' => 'supersecret']); $this->dispatch('/auth/login/provider/12?code=1233&state='.$this->_state); } @@ -656,6 +672,8 @@ class IdentityProviderAuthenticationCallbackAuthentication ['id' => 12, 'label' => 'FranceConnect', 'type' => 'franceconnect', + 'client_id' => 'myclientid', + 'client_secret' => 'supersecret' ]); } diff --git a/tests/scenarios/PnbDilicom/PnbDilicomTest.php b/tests/scenarios/PnbDilicom/PnbDilicomTest.php index b089e263bf1..20a9a6d1d2b 100644 --- a/tests/scenarios/PnbDilicom/PnbDilicomTest.php +++ b/tests/scenarios/PnbDilicom/PnbDilicomTest.php @@ -3936,6 +3936,7 @@ class PnbDilicomAdminIndexControllerTest extends AbstractControllerTestCase { 'login' => 'super admin', 'password' => 'pwd', 'role_level' => ZendAfi_Acl_AdminControllerRoles::SUPER_ADMIN]); + ZendAfi_Auth::getInstance()->logUser($super_admin); Class_WebService_BibNumerique_Dilicom_Hub::setDefaultHttpClient($this @@ -3949,7 +3950,6 @@ class PnbDilicomAdminIndexControllerTest extends AbstractControllerTestCase { Class_AdminVar::set('DILICOM_PNB_PWD_COLLECTIVITE', 'secretPassword'); Class_AdminVar::set('DILICOM_PNB_SERVER_URL', 'https://pnb-test.centprod.com'); Class_AdminVar::set('DILICOM_PNB_GLN_CONTRACTOR', 123456789); - Class_AdminVar::set('DILICOM_PNB_IP_ADRESSES', '127.0.0.1'); $this->dispatch('/admin/index/index', true); diff --git a/tests/scenarios/RGPD/PatronDownloadDatasTest.php b/tests/scenarios/RGPD/PatronDownloadDatasTest.php index 33794b50fa6..e95c7a512fa 100644 --- a/tests/scenarios/RGPD/PatronDownloadDatasTest.php +++ b/tests/scenarios/RGPD/PatronDownloadDatasTest.php @@ -90,6 +90,13 @@ class RGPD_PatronDowloadDatasTest extends AbstractControllerTestCase { $this->_createMultimediaHold(); } + + public function tearDown() { + Class_WebService_BibNumerique_Dilicom_Hub::setHttpClient(null); + parent::tearDown(); + } + + protected function _createArticle() { $this->fixture('Class_Article', ['id' => 10, @@ -245,14 +252,6 @@ class RGPD_PatronDowloadDatasTest extends AbstractControllerTestCase { 'record_origin_id' => $book->getIdOrigine(), 'order_line_id' => 'x321', 'expected_return_date' => '2022-06-01 20:10:00']); - - - } - - - public function tearDown() { - Class_WebService_BibNumerique_Dilicom_Hub::setDefaultHttpClient(null); - parent::tearDown(); } diff --git a/tests/scenarios/Templates/TemplatesAddWidgetTest.php b/tests/scenarios/Templates/TemplatesAddWidgetTest.php new file mode 100644 index 00000000000..fa9b59dd04e --- /dev/null +++ b/tests/scenarios/Templates/TemplatesAddWidgetTest.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +require_once 'TemplatesTest.php'; + + +class TemplatesAddWidgetSimpleTest extends TemplatesIntonationTestCase { + /** @test */ + public function addWidgetShouldRenderFreeWidget() { + $this->dispatch('/admin/widget/add-from-template/id_profil/72'); + $this->assertXPathContentContains('//div', 'Contenu HTML'); + } + + + /** @test */ + public function shouldContainsAdminToolsJpg() { + $this->dispatch('/admin/widget/add-from-template/id_profil/72'); + $this->assertXPath('//div//img[contains(@src, "/TEMPLATE_ADMIN_TOOLS.jpg")]'); + } + + + /** @test */ + public function addWidgetWithConfShouldRedirect() { + $this->dispatch('/admin/widget/add/after/20/division/2/id_profil/72/template/0/template_no/1'); + $this->assertRedirect(); + } +} + + + + +class TemplatesAddWidgetIdentityProviderEnabledTest extends TemplatesIntonationTestCase { + public function setUp() { + parent::setUp(); + Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1); + } + + + /** @test */ + public function shouldDisplayIdentityProviderWidgetChoice() { + $this->dispatch('/admin/widget/add-from-template/id_profil/72'); + $this->assertXPathContentContains('//div', 'Se connecter avec'); + } +} + + + + +class TemplatesAddWidgetIdentityProviderDisabledTest extends TemplatesIntonationTestCase { + public function setUp() { + parent::setUp(); + Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 0); + } + + + /** @test */ + public function shouldDisplayIdentityProviderWidgetChoice() { + $this->dispatch('/admin/widget/add-from-template/id_profil/72'); + $this->assertNotXPathContentContains('//div', 'Se connecter avec'); + } +} \ No newline at end of file diff --git a/tests/scenarios/Templates/TemplatesAuthLoginTest.php b/tests/scenarios/Templates/TemplatesAuthLoginTest.php new file mode 100644 index 00000000000..fb6e4ed1114 --- /dev/null +++ b/tests/scenarios/Templates/TemplatesAuthLoginTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +require_once 'TemplatesTest.php'; + + +class TemplatesAuthLoginWithIdentityProviderTest extends TemplatesIntonationTestCase { + /** @test */ + public function formActionShouldContainProvider() { + Zendafi_Auth::getInstance()->clearIdentity(); + $this->dispatch('/opac/auth/login/id_profil/72/provider/1?ST-P2908128716'); + $this->assertXPath('//form[contains(@action, "/provider/1")]'); + } +} + + + + +class TemplatesAuthLoginWidgetTest extends TemplatesIntonationTestCase { + + public function setUp() { + parent::setUp(); + Zendafi_Auth::getInstance()->clearIdentity(); + + $cfg_accueil = Class_Profil::find(72)->getCfgAccueilAsArray(); + $cfg_accueil['modules'][10]['preferences']['IntonationFormStyle'] = 'toggle'; + Class_Profil::find(72)->setCfgAccueil($cfg_accueil); + + $this->dispatch('/opac/auth/login/id_profil/72'); + } + + + /** @test */ + public function widgetShouldHaveDefaultDisplayOption() { + $this->assertNotXPath('//div[@data-action="auth_login"]//div[@class="dropdown"]'); + } + + + /** @test */ + public function shouldContainBooststrapContainer() { + $this->assertXPath('//div[@data-action="auth_login"]//div[@class="justify-content-center row no-gutters"]'); + } + + + /** @test */ + public function shouldContainUsernameInput() { + $this->assertXPath('//div[@data-action="auth_login"]//input[@name="username"]'); + } + + + /** @test */ + public function shouldContainTitleMonCompte() { + $this->assertXPathContentContains('//div[@data-action="auth_login"]//h2', 'Se connecter'); + } +} \ No newline at end of file diff --git a/tests/scenarios/Templates/TemplatesAuthorTest.php b/tests/scenarios/Templates/TemplatesAuthorTest.php index c68e499b475..43a7f4b673e 100644 --- a/tests/scenarios/Templates/TemplatesAuthorTest.php +++ b/tests/scenarios/Templates/TemplatesAuthorTest.php @@ -22,7 +22,7 @@ require_once('TemplatesTest.php'); -class TemplatesDispatchEditAuthorWidgetTest extends TemplatesIntonationTestCase { +class TemplatesAuthorEditWidgetTest extends TemplatesIntonationTestCase { public function setUp() { parent::setUp(); $this->dispatch('/admin/widget/edit-widget/id/23/id_profil/72', true); @@ -38,7 +38,7 @@ class TemplatesDispatchEditAuthorWidgetTest extends TemplatesIntonationTestCase -class TemplatesDispatchIntonationWithAuthorWidgetTest extends TemplatesIntonationTestCase { +class TemplatesAuthorWidgetTest extends TemplatesIntonationTestCase { /** @test */ public function rahanAuthorShouldBePresent() { @@ -114,7 +114,7 @@ abstract class TemplatesIntonationWithAuthorTest extends TemplatesIntonationTest -class TemplatesDispatchAuthorActionsTest extends TemplatesIntonationWithAuthorTest { +class TemplatesAuthorActionsTest extends TemplatesIntonationWithAuthorTest { public function setUp() { parent::setUp(); @@ -206,7 +206,7 @@ class TemplatesDispatchAuthorActionsTest extends TemplatesIntonationWithAuthorTe -class TemplatesDispatchAuthorDisabledBiographyTest extends TemplatesIntonationWithAuthorTest { +class TemplatesAuthorDisabledBiographyTest extends TemplatesIntonationWithAuthorTest { public function setUp() { parent::setUp(); diff --git a/tests/scenarios/Templates/TemplatesPatronConfigurationsTest.php b/tests/scenarios/Templates/TemplatesPatronConfigurationsTest.php new file mode 100644 index 00000000000..65580bb8090 --- /dev/null +++ b/tests/scenarios/Templates/TemplatesPatronConfigurationsTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +require_once 'TemplatesTest.php'; + +class TemplatesPatronConfigurationsIdentityProvidersNoneAssociatedTest + extends TemplatesIntonationAccountTestCase { + + public function setUp() { + parent::setUp(); + Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1); + $this->dispatch('/opac/abonne/configurations/id_profil/72'); + } + + + /** @test */ + public function pageShouldContainIdentityProvidersSection() { + $this->assertXPathContentContains('//h3', 'Fournisseur(s) d\'identité associé(s)'); + } + + + /** @test */ + public function pageShouldContainIdentitiesTable() { + $this->assertXPath('//table[@id="active_identities"]'); + } +} + + + + +class TemplatesPatronConfigurationsIdentityProvidersOneAssociatedTest + extends TemplatesIntonationAccountTestCase { + + public function setUp() { + parent::setUp(); + + Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1); + + $this->fixture('Class_User_Identity', + ['id' => 1, + 'provider_id' => 1, + 'user_id' => Class_Users::getIdentity()->getId(), + 'identifier' => 'mysuperid']); + + $this->fixture('Class_IdentityProvider', + ['id' => 1, + 'label' => 'CAS 2.0', + 'type' => 'cas2', + 'url' => 'http://moncompte.server.com/', + ]); + + $this->dispatch('/opac/abonne/configurations/id_profil/72'); + } + + + /** @test */ + public function pageShouldContainIdentityProvidersSection() { + $this->assertXPathContentContains('//h3', 'Fournisseur(s) d\'identité associé(s)'); + } + + + /** @test */ + public function pageShouldContainIdentityCas2() { + $this->assertXPathContentContains('//table[@id="active_identities"]//td', 'CAS 2.0'); + } +} diff --git a/tests/scenarios/Templates/TemplatesTest.php b/tests/scenarios/Templates/TemplatesTest.php index b458e54cb4a..e886c6bb0c1 100644 --- a/tests/scenarios/Templates/TemplatesTest.php +++ b/tests/scenarios/Templates/TemplatesTest.php @@ -920,30 +920,6 @@ class TemplatesDispatchIntonationSearchTest extends TemplatesIntonationTestCase -class TemplatesMultipleDispatchTest extends TemplatesEnabledTestCase { - protected $_storm_default_to_volatile = true; - - public function setUp() { - parent::setUp(); - $this->fixture('Class_Profil', - ['id' => 5, - 'template' => 'INTONATION']); - $this->dispatch('/opac/index/index/id_profil/5', true); - $this->dispatch('/opac/recherche/simple/pomme', true); - $this->dispatch('/opac/index/index/id_profil/2', true); - $this->dispatch('/opac/index/index/id_profil/5', true); - } - - - /** @test */ - public function currentProfilTemplateShouldBeIntonation() { - $this->assertEquals('INTONATION', Class_Profil::getCurrentProfil()->getTemplate()); - } -} - - - - class TemplatesEditTest extends TemplatesEnabledTestCase { protected $_storm_default_to_volatile = true; @@ -3556,7 +3532,7 @@ class TemplatesDispatchIntonationAuthLoginWithRedirectTest extends TemplatesInto /** @test */ public function formActionShouldContainsRedirectUrlMyBib() { - $this->assertXPath('//form[contains(@action, "/auth/login/redirect/'.urlencode("http://mybib/modules/skilleos").'")]', $this->_response->getBody()); + $this->assertXPath('//form[contains(@action, "/auth/login/id_profil/72/redirect/'.urlencode("http://mybib/modules/skilleos").'")]', $this->_response->getBody()); } diff --git a/tests/scenarios/Templates/TemplatesWidgetIdentityProvidersTest.php b/tests/scenarios/Templates/TemplatesWidgetIdentityProvidersTest.php new file mode 100644 index 00000000000..ade229f98e9 --- /dev/null +++ b/tests/scenarios/Templates/TemplatesWidgetIdentityProvidersTest.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright (c) 2012-2020, Agence Française Informatique (AFI). All rights reserved. + * + * BOKEH is free software; you can redistribute it and/or modify + * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by + * the Free Software Foundation. + * + * There are special exceptions to the terms and conditions of the AGPL as it + * is applied to this software (see README file). + * + * BOKEH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE + * along with BOKEH; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +require_once 'TemplatesTest.php'; + + +class TemplatesWidgetIdentityProvidersTest extends TemplatesIntonationTestCase { + /** @test */ + public function shouldContainBootstrapedButton() { + Class_AdminVar::set('ENABLE_IDENTITY_PROVIDERS', 1); + + $this->fixture('Class_IdentityProvider', + ['id' => 1, + 'label' => 'CAS 2.0', + 'type' => 'cas2', + 'url' => 'http://moncompte.server.com/', + ]); + + Zendafi_Auth::getInstance()->clearIdentity(); + + $cfg_accueil = Class_Profil::find(72)->getCfgAccueilAsArray(); + $cfg_accueil['modules'][] = ['division' => 4, + 'type_module' => 'IDENTITY_PROVIDER', + 'preferences' => []]; + Class_Profil::find(72)->setCfgAccueil($cfg_accueil); + + $this->dispatch('/opac/index/index/id_profil/72'); + + $this->assertXPathContentContains('//div[contains(@class, "justify-content-center")]//button', + 'CAS 2.0'); + } +} diff --git a/tests/scenarios/Templates/TemplatesWidgetTest.php b/tests/scenarios/Templates/TemplatesWidgetTest.php index 70616a0b1ba..48e5008d3e9 100644 --- a/tests/scenarios/Templates/TemplatesWidgetTest.php +++ b/tests/scenarios/Templates/TemplatesWidgetTest.php @@ -60,32 +60,6 @@ class TemplatesDispatchNewsletterWidgetTest extends TemplatesIntonationTestCase -class TemplatesDispatchWidgetAddWidgetTest extends TemplatesIntonationTestCase { - - /** @test */ - public function addWidgetShouldRenderFreeWidget() { - $this->dispatch('/admin/widget/add-from-template/id_profil/72'); - $this->assertXPathContentContains('//div', 'Contenu HTML'); - } - - - /** @test */ - public function shouldContainsAdminToolsJpg() { - $this->dispatch('/admin/widget/add-from-template/id_profil/72'); - $this->assertXPath('//div//img[contains(@src, "/TEMPLATE_ADMIN_TOOLS.jpg")]'); - } - - - /** @test */ - public function addWidgetWithConfShouldRedirect() { - $this->dispatch('/admin/widget/add/after/20/division/2/id_profil/72/template/0/template_no/1'); - $this->assertRedirect(); - } -} - - - - class TemplatesDispatchMenuWidgetTest extends TemplatesIntonationTestCase { /** @test */ -- GitLab