diff --git a/.gitmodules b/.gitmodules index 59c95bea89e947f98ba61bf7f8e3e12e039789cd..a17826c40d12815d562853c5a940aa40a339ab71 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,9 @@ [submodule "library/matomo-php-tracker"] path = library/matomo-php-tracker url = http://git.afi-sa.fr/afi/matomo-php-tracker.git +[submodule "library/phpseclib"] + path = library/phpseclib + url = https://git.afi-sa.net/afi/phpseclib.git +[submodule "library/activitystreams"] + path = library/activitystreams + url = https://git.afi-sa.net/afi/activitystreams.git diff --git a/VERSIONS_WIP/93553 b/VERSIONS_WIP/93553 new file mode 100644 index 0000000000000000000000000000000000000000..8da49c3bd45147c062b43fa549b27dfa873fabf1 --- /dev/null +++ b/VERSIONS_WIP/93553 @@ -0,0 +1 @@ + - ticket #93553 : Avis : Ajout d'une fédération des avis \ No newline at end of file diff --git a/application/modules/activitypub/controllers/ReviewController.php b/application/modules/activitypub/controllers/ReviewController.php new file mode 100644 index 0000000000000000000000000000000000000000..eb25b048281f79b194e697800bf74bef89c38896 --- /dev/null +++ b/application/modules/activitypub/controllers/ReviewController.php @@ -0,0 +1,310 @@ +<?php +/** + * Copyright (c) 2012-2017, 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 __DIR__ . '/../../../../library/activitystreams/autoload.php'; + +use Patbator\ActivityStreams\Model\Join; +use Patbator\ActivityStreams\Model\Leave; +use Patbator\ActivityStreams\Model\Accept; +use Patbator\ActivityStreams\Model\Reject; +use Patbator\ActivityStreams\Model\CollectionPage; +use Patbator\ActivityStreams\Stream; + + +class Activitypub_ReviewController extends ZendAfi_Controller_Action { + use Trait_TimeSource; + + public function preDispatch() { + parent::preDispatch(); + + $this->getHelper('ViewRenderer')->setNoRender(); + $this->_log = null;//new Zend_Log(new Zend_Log_Writer_Stream(PATH_TEMP . 'activitypub_server.log')); + } + + + public function unavailableAction() { + $this->_response->setHttpResponseCode(503); + } + + + public function indexAction() { + if (Class_WebService_ActivityPub::MIME_TYPE != $this->_request->getHeader('Accept')) + return $this->_response->setHttpResponseCode(500); + + $service = $this->_service()->asActor() + + ->inbox($this->_absoluteUrl(['module' => 'activitypub', + 'controller' => 'review', + 'action' => 'inbox'])) + + ->outbox($this->_absoluteUrl(['module' => 'activitypub', + 'controller' => 'review', + 'action' => 'outbox'])) + + ->publicKey($this->_absoluteUrl(['module' => 'activitypub', + 'controller' => 'review', + 'action' => 'pubkey'])) + ; + + $this->_activityResponseWith($service); + } + + + public function pubkeyAction() { + if (Class_WebService_ActivityPub::MIME_TYPE != $this->_request->getHeader('Accept')) + return $this->_response->setHttpResponseCode(500); + + $key = (new Class_ActivityPub_PublicKey) + ->id($this->_absoluteUrl(['module' => 'activitypub', + 'controller' => 'review', + 'action' => 'pubkey'])) + + ->owner($this->_absoluteUrl(['module' => 'activitypub', + 'controller' => 'review'])) + + ->publicKeyPem((new Class_Federation())->getPublicKey()); + + $this->_activityResponseWith($key); + } + + + public function inboxAction() { + if (!$this->_request->isPost() + || Class_WebService_ActivityPub::MIME_TYPE != $this->_request->getHeader('Content-Type')) + return $this->_response->setHttpResponseCode(500); + + if (!$signature = $this->_request->getHeader('Signature')) + return $this->_response->setHttpResponseCode(400); + + $rawBody = $this->_request->getRawBody(); + + if ((!$activity = Stream::fromJson($rawBody)->getRoot()) + || (!$actor = $activity->actor())) + return $this->_response->setHttpResponseCode(400); + + $service = (new Class_WebService_ActivityPub($actor->id())) + ->setLogger($this->_log); + + if (!$service->validateRequest($this->_request)) + return $this->_response->setHttpResponseCode(400); + + if ($handler = Activitypub_ReviewController_GroupHandler::handlerFor($activity)) { + $response = $handler->handle() + ->actor($this->_service()); + return $this->_activityResponseWith($response); + } + + $this->_response->setHttpResponseCode(400); + } + + + public function outboxAction() { + if (Class_WebService_ActivityPub::MIME_TYPE != $this->_request->getHeader('Accept')) + return $this->_response->setHttpResponseCode(500); + + if ((!$auth = $this->_request->getHeader('Authorization')) + || (!$bearer = trim(str_replace('Bearer ', '', $auth)))) + return $this->_response->setHttpResponseCode(403); + + if ($key = $this->_getParam('key')) + return $this->_receiveRecordQueryFrom($key, $bearer); + + if (Class_AdminVar::get('FEDERATION_COMMUNITY_SERVER') != $bearer) + return $this->_response->setHttpResponseCode(403); + + $filters = ['abon_ou_bib' => Class_AvisNotice::TYPE_LIBRARIAN]; + if (Class_AdminVar::isLibrarianReviewsModerated()) + $filters['statut'] = Class_AvisNotice::STATUS_VALIDATED; + + if ($from = $this->_getParam('from')) { + if (false === $from_date = DateTime::createFromFormat('Y-m-d', $from)) + return $this->_response->setHttpResponseCode(400); + + $filters['where'] = 'date_avis >= "' . $from_date->format('Y-m-d') . '"'; + } + + $this->_reviewPageFilteredBy($filters); + } + + + protected function _receiveRecordQueryFrom($key, $bearer) { + if (!Class_Federation_GroupMembership::findFirstBy(['actor_id' => $bearer, + 'group_name' => 'REVIEW_DISPLAY'])) + return $this->_response->setHttpResponseCode(403); + + $filters = ['clef_oeuvre' => $key, 'source_actor_id not' => null]; + + $this->_reviewPageFilteredBy($filters, ['key' => $key]); + } + + + protected function _reviewPageFilteredBy($filters, $url_params=[]) { + $items_by_page = 15; + $total = Class_AvisNotice::countBy($filters); + $page = $this->_getParam('page', 1); + $models = Class_AvisNotice::findAllBy(array_merge($filters, + ['limitPage' => [$page, $items_by_page]])); + $items = array_map([$this, '_filterAttributes'], $models); + + $activity = (new CollectionPage) + ->id($this->_absoluteUrl(array_merge(['module' => 'activitypub', + 'controller' => 'review', + 'action' => 'outbox', + 'page' => $page], + $url_params))) + ->totalItems($total) + ->items($items); + + $this->_activityResponseWith($activity); + } + + + protected function _filterAttributes($review) { + return ['id' => $review->getId(), + 'clef_oeuvre' => $review->getClefOeuvre(), + 'date_avis' => $review->getDateAvis(), + 'date_mod' => $review->getDateMod(), + 'note' => $review->getNote(), + 'entete' => $review->getEntete(), + 'avis' => $review->getAvis(), + 'abon_ou_bib' => $review->getAbonOuBib(), + 'source_author' => $review->getSourceAuthor()]; + } + + + protected function _absoluteUrl($params) { + return Class_Url::absolute($params, null, true, false); + } + + + protected function _service() { + return (new Class_ActivityPub_Service()) + ->id($this->_absoluteUrl(['module' => 'activitypub', + 'controller' => 'review'])) + ->name((new Class_Federation())->getActorName()); + } + + + protected function _activityResponseWith($activity) { + (new Class_WebService_ActivityPubServer($this->_absoluteUrl(['module' => 'activitypub', + 'controller' => 'review']))) + ->setLogger($this->_log) + ->respondTo($this->_request, $this->_response, $activity); + } +} + + + + +abstract class Activitypub_ReviewController_GroupHandler { + protected $_group_name, $_actor_id, $_activity_id; + + public static function handlerFor($activity) { + if (!$activity->object() + || (!$group_name = $activity->object()->name())) + return; + + if ($activity instanceof Join) + return new Activitypub_ReviewController_JoinHandler($activity); + + if ($activity instanceof Leave) + return new Activitypub_ReviewController_LeaveHandler($activity); + } + + + public function __construct($activity) { + $this->_group_name = $activity->object()->name(); + $this->_actor_id = $activity->actor()->id(); + $this->_activity_id = $activity->id(); + } + + + protected function _findMembership() { + return Class_Federation_GroupMembership::findFirstBy($this->_membershipParams()); + } + + + protected function _newMembership() { + return Class_Federation_GroupMembership::newInstance($this->_membershipParams()); + } + + + protected function _membershipParams() { + return ['actor_id' => $this->_actor_id, + 'group_name' => $this->_group_name]; + } + + + protected function _reject($message) { + return (new Reject)->summary($message); + } + + + protected function _accept($member) { + return (new Accept)->id(Class_Url::absolute(['module' => 'activitypub', + 'controller' => 'review', + 'action' => 'group-membership', + 'id' => $member->getId()], + null, true, false)); + } + + + public function handle() { + return Class_AdminVar::isFederationCommunityServer() + ? $this->_handle() + : $this->_reject('Service unavailable'); + } + + + abstract protected function _handle(); +} + + + + +class Activitypub_ReviewController_JoinHandler extends Activitypub_ReviewController_GroupHandler { + protected function _handle() { + if (!$member = $this->_findMembership()) + $member = $this->_newMembership(); + + $response = ($member->isNew() && !$member->save()) + ? $this->_reject(implode(', ', $member->getErrors())) + : $this->_accept($member); + + return $response->object((new Join)->id($this->_activity_id)); + } +} + + + + +class Activitypub_ReviewController_LeaveHandler extends Activitypub_ReviewController_GroupHandler { + protected function _handle() { + if ($member = $this->_findMembership()) + $member->delete(); + + $response = (!$member) + ? $this->_reject('Cannot leave a group you did not join') + : $this->_accept($member); + + return $response->object((new Leave)->id($this->_activity_id)); + } +} \ No newline at end of file diff --git a/application/modules/admin/controllers/FederationReviewsController.php b/application/modules/admin/controllers/FederationReviewsController.php new file mode 100644 index 0000000000000000000000000000000000000000..524c2953410c79c4925b560a1a1b639f62a6b26f --- /dev/null +++ b/application/modules/admin/controllers/FederationReviewsController.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright (c) 2012-2017, 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 Admin_FederationReviewsController extends ZendAfi_Controller_Action { + public function indexAction() { + $this->view->titre = $this->_('Avis communautaires'); + } + + + public function enableDisplayAction() { + $federation = Class_FederationReview::getInstance(); + $message = $federation->enableDisplay() + ? $this->_('Affichage des avis communautaires activé') + : $this->_('Activation impossible : %s', + $federation->getLastMessage()); + + $this->_helper->notify($message); + $this->_redirectToIndex(); + } + + + public function disableDisplayAction() { + Class_FederationReview::getInstance()->disableDisplay(); + $this->_helper->notify($this->_('Affichage des avis communautaires désactivé')); + $this->_redirectToIndex(); + } + + + public function enableShareAction() { + $federation = Class_FederationReview::getInstance(); + $message = $federation->enableShare() + ? $this->_('Partage des avis à la communauté activé') + : $this->_('Activation impossible : %s', + $federation->getLastMessage()); + + $this->_helper->notify($message); + $this->_redirectToIndex(); + } + + + public function disableShareAction() { + Class_FederationReview::getInstance()->disableShare(); + $this->_helper->notify($this->_('Partage des avis à la communauté désactivé')); + $this->_redirectToIndex(); + } +} \ No newline at end of file diff --git a/application/modules/admin/controllers/JournalController.php b/application/modules/admin/controllers/JournalController.php new file mode 100644 index 0000000000000000000000000000000000000000..3920b3ae52e864a8b159a83f2aaeb782458e9876 --- /dev/null +++ b/application/modules/admin/controllers/JournalController.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright (c) 2012-2017, 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 Admin_JournalController extends ZendAfi_Controller_Action { + + public function getPlugins() { + return ['ZendAfi_Controller_Plugin_ResourceDefinition_Journal']; + } +} \ No newline at end of file diff --git a/application/modules/admin/views/scripts/federation-reviews/index.phtml b/application/modules/admin/views/scripts/federation-reviews/index.phtml new file mode 100644 index 0000000000000000000000000000000000000000..40af77a22a6762bd503072187d4e3be3a0f5f208 --- /dev/null +++ b/application/modules/admin/views/scripts/federation-reviews/index.phtml @@ -0,0 +1,23 @@ +<?php +echo $this->tag('p', + $this->_('Bokeh peut se connecter à un serveur communautaire de partage d\'avis.')); + +$this->disable_onchange = true; + +$federation_review = Class_FederationReview::getInstance(); + +echo $federation_review->isDisplayEnabled() + ? $this->Button_Cancel((new Class_Entity()) + ->setText($this->_('Désactiver l\'affichage des avis communautaires')) + ->setUrl($this->url(['action' => 'disable-display']))) + : $this->Button_New((new Class_Entity()) + ->setText($this->_('Activer l\'affichage des avis communautaires')) + ->setUrl($this->url(['action' => 'enable-display']))); + +echo $federation_review->isShareEnabled() + ? $this->Button_Cancel((new Class_Entity()) + ->setText($this->_('Désactiver l\'envoi des avis de ce portail à la communauté')) + ->setUrl($this->url(['action' => 'disable-share']))) + : $this->Button_New((new Class_Entity()) + ->setText($this->_('Activer l\'envoi des avis de ce portail à la communauté')) + ->setUrl($this->url(['action' => 'enable-share']))); diff --git a/application/modules/opac/controllers/AbonneController.php b/application/modules/opac/controllers/AbonneController.php index e2cfe91a7c40d9d1fab994277c4427e2c0ca4b70..f068b135d0cc0f8ad958cd5619eb2d1228f53c90 100644 --- a/application/modules/opac/controllers/AbonneController.php +++ b/application/modules/opac/controllers/AbonneController.php @@ -181,20 +181,22 @@ class AbonneController extends ZendAfi_Controller_Action { $this->_user ->setPseudo($this->_request->getParam('avisSignature')) ->save(); + $this->_helper->notify($this->_('Votre avis à bien été enregistré')); + return $this->_redirectClose($this->_getReferer()); } $this->view->message = implode('.', $avis->getErrors()); } - if ($avis != null) { $this->view->id = $avis->getId(); $this->view->avisEntete = $avis->getEntete(); $this->view->avisTexte = $avis->getAvis(); $this->view->avisNote = $avis->getNote(); } + $this->view->avisSignature = $this->_user->getNomAff(); $this->view->id_notice = $id_notice; } diff --git a/cosmogramme/sql/patch/patch_379.php b/cosmogramme/sql/patch/patch_379.php new file mode 100644 index 0000000000000000000000000000000000000000..d3218fe18584f6b5a0ea813ee323145618e5866a --- /dev/null +++ b/cosmogramme/sql/patch/patch_379.php @@ -0,0 +1,56 @@ +<?php +$adapter = Zend_Db_Table_Abstract::getDefaultAdapter(); + +try { + $adapter->query( + 'CREATE TABLE `journal` (' + . '`id` int(11) unsigned not null auto_increment,' + . '`type` varchar(255) not null,' + . '`created_at` datetime not null,' + . 'primary key (id),' + . 'key `type` (`type`),' + . 'key `created_at` (`created_at`)' + . ') engine=MyISAM default charset=utf8' + ); +} catch(Exception $e) {} + +try { + $adapter->query( + 'CREATE TABLE `journal_detail` (' + . '`id` int(11) unsigned not null auto_increment,' + . '`journal_id` int(11) unsigned not null,' + . '`type` varchar(255) not null,' + . '`value` text not null,' + . 'primary key (id),' + . 'key `journal_id` (`journal_id`),' + . 'key `type` (`type`)' + . ') engine=MyISAM default charset=utf8' + ); +} catch(Exception $e) {} + +try { + $adapter->query( + 'CREATE TABLE `federation_group_membership` (' + . '`id` int(11) unsigned not null auto_increment,' + . '`group_name` varchar(255) not null,' + . '`actor_id` varchar(255) not null,' + . '`accepted_at` datetime not null,' + . 'primary key (id),' + . 'key `accepted_at` (`accepted_at`),' + . 'key `group_name` (`group_name`),' + . 'key `actor_id` (`actor_id`)' + . ') engine=MyISAM default charset=utf8' + ); +} catch(Exception $e) {} + +try { + $adapter->query( + 'ALTER TABLE `notices_avis` ' + . 'ADD COLUMN `source_actor_id` varchar(255) null default null,' + . 'ADD COLUMN `source_author` varchar(255) null default null,' + . 'ADD COLUMN `source_primary` int(11) null default null,' + . 'ADD KEY `source_actor_id` (`source_actor_id`),' + . 'ADD KEY `source_author` (`source_author`),' + . 'ADD KEY `source_primary` (`source_primary`)' + ); +} catch(Exception $e) {} diff --git a/doc/extern_libs.org b/doc/extern_libs.org index aad4fa9a7747d314b288a62166a10e0827f69741..c4fca33445cda79b2cad7c0a22be4437ef516400 100644 --- a/doc/extern_libs.org +++ b/doc/extern_libs.org @@ -45,4 +45,6 @@ | icon slideshow by Javier Cabezas | CCBY | | editeur d'articles | | https://thenounproject.com/term/slideshow/6517/ | | PHP-Parser | BSD-3-Clauses | - | validation de fichiers php (formulaires de recherche) | | https://github.com/nikic/PHP-Parser | | Jquery Notification | MIT ? | | barre bleue de notification | oui (barre en bas) | n'existe plus | +| activitystreams | MIT | | Avis communautaires | | https://gitlab.com/patbator/activitystreams | +| phpseclib | MIT | | Avis communautaires | X ajout d'un autoload.php | https://github.com/phpseclib/phpseclib | | leaflet.fullscreen | MIT | | bouton plein écran sur la carte des bibliothèques | | https://github.com/brunob/leaflet.fullscreen | diff --git a/library/Class/ActivityPub/PublicKey.php b/library/Class/ActivityPub/PublicKey.php new file mode 100644 index 0000000000000000000000000000000000000000..c8bcd98424171f7bd6ca322a50ed815ac9207af3 --- /dev/null +++ b/library/Class/ActivityPub/PublicKey.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright (c) 2012-2019, 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 __DIR__ . '/../../activitystreams/autoload.php'; + +use \Patbator\ActivityStreams\Model\Base; + + +class Class_ActivityPub_PublicKey extends Base { + protected $_attribs = ['id' => null, + 'type' => 'Key', + 'owner' => null, + 'publicKeyPem' => null]; +} diff --git a/library/Class/ActivityPub/Service.php b/library/Class/ActivityPub/Service.php new file mode 100644 index 0000000000000000000000000000000000000000..294d8a2611f79af6d4cfcb1624f5d1b7d56560e3 --- /dev/null +++ b/library/Class/ActivityPub/Service.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright (c) 2012-2019, 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 __DIR__ . '/../../activitystreams/autoload.php'; + +use \Patbator\ActivityStreams\Model\Service; + + +class Class_ActivityPub_Service extends Service { + public function __construct() { + parent::__construct(); + $this->_actor_attribs[] = 'publicKey'; + } + + + public function type() { + return 'Service'; + } +} diff --git a/library/Class/AdminVar.php b/library/Class/AdminVar.php index f011c795fa9525f6dd2dff344e70c695145c64ef..7d3429f41812d860cacd015c1f89364a40647e38 100644 --- a/library/Class/AdminVar.php +++ b/library/Class/AdminVar.php @@ -136,6 +136,7 @@ class Class_AdminVarLoader extends Storm_Model_Loader { 'static_map' => $this->_getStaticMapVars(), 'search' => $this->_getSearchVars(), 'file-manager' => $this->_getFileManagerVars(), + 'federation-reviews' => $this->_getFederationVars(), 'usergroup-agenda' => $this->_getRendezVousVars(), ]; } @@ -490,6 +491,16 @@ class Class_AdminVarLoader extends Storm_Model_Loader { } + protected function _getFederationVars() { + return ['FEDERATION_COMMUNITY_SERVER' => Class_AdminVar_Meta::newDefault($this->_('URL du serveur communautaire de la fédération, vide pour désactiver')), + 'FEDERATION_ACTOR_NAME' => Class_AdminVar_Meta::newDefault($this->_('Nom d\'affichage de ce Bokeh dans la communautée. Si vide, l\'url sera utilisée')), + 'FEDERATION_IS_COMMUNITY_SERVER' => Class_AdminVar_Meta::newOnOff($this->_('Ce Bokeh est un serveur communautaire')), + 'FEDERATION_PUBKEY' => Class_AdminVar_Meta::newCryptKey($this->_('Clé publique permettant la vérification de signature des messages envoyés par ce Bokeh à la fédération')), + 'FEDERATION_PRIVKEY' => Class_AdminVar_Meta::newCryptKey($this->_('Clé privée permettant de générer les signatures des messages envoyés par ce Bokeh à la fédération')), + ]; + } + + protected function _getRendezVousVars() { return ['ENABLE_RENDEZ_VOUS' => Class_AdminVar_Meta::newOnOff($this->_('Activer la gestion des rendez-vous')), 'NOTIFICATION_TEMPLATE_RENDEZ_VOUS' => Class_AdminVar_Meta::newEditor($this->_('Modèle utilisé pour les courriels de notifications de rendez-vous.'), @@ -1052,6 +1063,26 @@ class Class_AdminVarLoader extends Storm_Model_Loader { } + public function isFederationCommunityServer() { + return Class_AdminVar::isModuleEnabled('FEDERATION_IS_COMMUNITY_SERVER'); + } + + + public function isFederationEnabled() { + return ('' != trim(Class_AdminVar::get('FEDERATION_COMMUNITY_SERVER'))); + } + + + public function isFederationServiceAvailable() { + return Class_AdminVar::isFederationEnabled() || Class_AdminVar::isFederationCommunityServer(); + } + + + public function isLibrarianReviewsModerated() { + return Class_AdminVar::isModuleEnabled('MODO_AVIS_BIBLIO'); + } + + public function isRendezVousEnabled() { return Class_AdminVar::isModuleEnabled('ENABLE_RENDEZ_VOUS'); } diff --git a/library/Class/AdminVar/Meta.php b/library/Class/AdminVar/Meta.php index 8cf86285c80532fcdb85dcbfbc1d8e4b251e514f..17d0e632ea24c3b0ed4a9d7512c6f59d1f13de4b 100644 --- a/library/Class/AdminVar/Meta.php +++ b/library/Class/AdminVar/Meta.php @@ -21,6 +21,8 @@ class Class_AdminVar_Meta { + use Trait_Translator; + const TYPE_DEFAULT = 'default', TYPE_ENCODED_DATA = 'encoded-data', @@ -28,7 +30,8 @@ class Class_AdminVar_Meta { TYPE_MULTI_INPUT = 'multi-input', TYPE_COMBO = 'combo', TYPE_RAW_TEXT = 'raw-text', - TYPE_EDITOR = 'editor'; + TYPE_EDITOR = 'editor', + TYPE_CRYPT_KEY = 'crypt-key'; protected @@ -37,13 +40,14 @@ class Class_AdminVar_Meta { public static function __callStatic($name, $args) { - $mapping = ['Default' => self::TYPE_DEFAULT, - 'EncodedData' => self::TYPE_ENCODED_DATA, - 'OnOff' => self::TYPE_ON_OFF, - 'MultiInput' => self::TYPE_MULTI_INPUT, - 'Combo' => self::TYPE_COMBO, - 'RawText' => self::TYPE_RAW_TEXT, - 'Editor' => self::TYPE_EDITOR]; + $mapping = ['Default' => static::TYPE_DEFAULT, + 'EncodedData' => static::TYPE_ENCODED_DATA, + 'OnOff' => static::TYPE_ON_OFF, + 'MultiInput' => static::TYPE_MULTI_INPUT, + 'Combo' => static::TYPE_COMBO, + 'RawText' => static::TYPE_RAW_TEXT, + 'Editor' => static::TYPE_EDITOR, + 'CryptKey' => static::TYPE_CRYPT_KEY]; $type_name = substr($name, 3); @@ -65,8 +69,14 @@ class Class_AdminVar_Meta { $this->_type = $type; $this->_description = $description; $this->_attributes = $attributes; + if (!isset($this->_attributes['role_level'])) $this->_attributes['role_level'] = ZendAfi_Acl_AdminControllerRoles::ADMIN_PORTAIL; + + if (!isset($this->_attributes['renderer']) && static::TYPE_CRYPT_KEY == $type) + $this->_attributes['renderer'] = function($value, $view) { + return $value ? $this->_('*** CLÉ MASQUÉE ***') : $this->_('*** VIDE ***'); + }; } diff --git a/library/Class/Avis.php b/library/Class/Avis.php index d47820117cabcd59b536cfbf920adf3c29a70d9d..72033c3c30c8b721200b9bc47f0733678fdf8315 100644 --- a/library/Class/Avis.php +++ b/library/Class/Avis.php @@ -123,6 +123,10 @@ class Class_Avis extends Storm_Model_Abstract { public function getFlags() { return -1; } -} -?> \ No newline at end of file + + /** API compatibility with Class_AvisNotice */ + public function getSourceAuthor() { + return null; + } +} diff --git a/library/Class/AvisNotice.php b/library/Class/AvisNotice.php index 2f7820332623152e0b5d46cff82ee6a3ef4e5a80..0de9864d578c9bb41f09a4ecb2c980e7f89ff1f5 100644 --- a/library/Class/AvisNotice.php +++ b/library/Class/AvisNotice.php @@ -239,9 +239,11 @@ class AvisNoticeLoader extends Storm_Model_Loader { class Class_AvisNotice extends Storm_Model_Abstract { use Trait_Avis, Trait_Translator; - const NO_FLAG=0; - const ORPHAN_FLAG=1; - const ARCHIVED_FLAG=2; + const NO_FLAG = 0; + const ORPHAN_FLAG = 1; + const ARCHIVED_FLAG = 2; + const STATUS_VALIDATED = 1; + const TYPE_LIBRARIAN = 1; protected $_loader_class = 'AvisNoticeLoader'; protected $_table_name = 'notices_avis'; @@ -520,6 +522,14 @@ class Class_AvisNotice extends Storm_Model_Abstract { } + public function acceptJournalVisitor($visitor) { + if ($author = $this->getUserName()) + $visitor->visitDetail('AUTHOR', $author); + + $visitor->visitDetail('NEEDS_VALIDATION', $this->shouldBeModerated()); + } + + public function isMine() { if (!$user = Class_Users::getIdentity()) return false; diff --git a/library/Class/Batch.php b/library/Class/Batch.php index 1901cd757af0ef839c403b3720360012e0a93e70..ff78f8f303a901ba68983f6825847325b349fdfc 100644 --- a/library/Class/Batch.php +++ b/library/Class/Batch.php @@ -41,8 +41,10 @@ class Class_BatchLoader extends Storm_Model_Loader { Class_Batch_BuildSiteMap::TYPE => new Class_Batch_BuildSiteMap(), Class_Batch_PremierChapitre::TYPE => new Class_Batch_PremierChapitre(), Class_Batch_NoveltyFacet::TYPE => new Class_Batch_NoveltyFacet(), + Class_Batch_ExternalAgenda::TYPE => new Class_Batch_ExternalAgenda(), + Class_Batch_FederationReviewHarvest::TYPE => new Class_Batch_FederationReviewHarvest(), Class_Batch_SendRendezVousNotification::TYPE => new Class_Batch_SendRendezVousNotification(), - Class_Batch_ExternalAgenda::TYPE => new Class_Batch_ExternalAgenda]); + ]); } diff --git a/library/Class/Batch/FederationReviewHarvest.php b/library/Class/Batch/FederationReviewHarvest.php new file mode 100644 index 0000000000000000000000000000000000000000..aafffe230560216317ef4562cef0326a8a034048 --- /dev/null +++ b/library/Class/Batch/FederationReviewHarvest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright (c) 2012-2019, 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_Batch_FederationReviewHarvest extends Class_Batch_Abstract { + const TYPE = 'FEDERATION_REVIEW_HARVEST'; + + public function getLabel() { + return $this->_('Moissonner les avis des portails Bokeh ayant activé le partage d\'avis'); + } + + + public function isEnabled() { + return Class_AdminVar::isFederationCommunityServer(); + } + + + public function run() { + foreach (Class_Federation_GroupMembership::findAllReviewShare() as $member) + $this->_harvestMember($member); + } + + + protected function _harvestMember($member) { + $service = new Class_WebService_ActivityPub($member->getActorId()); + if (!$service->isValid()) + return; + + $journal_type = $member->getHarvestJournalType(); + $from = ($last = Class_Journal::lastOf($journal_type)) + ? $last->getCreatedAt() + : ''; + + $reviews = $service->harvestReviews($from); + if (!$reviews && $service->getLastMessage()) { + // todo log message + return; + } + + $service_name = $service->name(); + foreach($reviews as $review) + $this->_harvestReviewOf($review, $member, $service_name); + + Class_Journal::factory($journal_type); + } + + + protected function _harvestReviewOf($review, $member, $name) { + $source_primary = $review['id']; + unset($review['id']); + $key_params = ['source_primary' => $source_primary, + 'source_actor_id' => $member->getActorId()]; + + if (!$model = Class_AvisNotice::findFirstBy($key_params)) + $model = Class_AvisNotice::newInstance($key_params); + + $review['source_author'] = $name; + $model->updateAttributes($review)->save(); + } +} diff --git a/library/Class/Federation.php b/library/Class/Federation.php new file mode 100644 index 0000000000000000000000000000000000000000..190dfade698e2a2b9d79007f246a3b288430bf96 --- /dev/null +++ b/library/Class/Federation.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright (c) 2012-2019, 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 'library/phpseclib/autoload.php'; + +use \phpseclib\Crypt\RSA; + +class Class_Federation { + const + PUBLIC_KEY = 'publickey', + PRIVATE_KEY = 'privatekey'; + + + public function getActorName() { + return ($name = Class_AdminVar::get('FEDERATION_ACTOR_NAME')) + ? $name + : Class_Url::absolute([], null, true); + } + + + public function getPrivateKey() { + return $this->_getOrGenerate('FEDERATION_PRIVKEY', 'privatePart'); + } + + + public function getPublicKey() { + return $this->_getOrGenerate('FEDERATION_PUBKEY', 'publicPart'); + } + + + protected function _getOrGenerate($var_name, $method_name) { + if ($key = Class_AdminVar::get($var_name)) + return $key; + + $pair = (new Class_Federation_KeyPair())->injectInAdminVars(); + return call_user_func([$pair, $method_name]); + } +} + + + +class Class_Federation_KeyPair { + const + PUBLIC_KEY = 'publickey', + PRIVATE_KEY = 'privatekey', + KEY_LENGTH = 2048; + + protected + $_private_part, + $_public_part; + + + public function __construct() { + $key = (new RSA())->createKey(static::KEY_LENGTH); + $this->_private_part = $key[static::PRIVATE_KEY]; + $this->_public_part = $key[static::PUBLIC_KEY]; + } + + + public function injectInAdminVars() { + Class_AdminVar::set('FEDERATION_PUBKEY', $this->_public_part); + Class_AdminVar::set('FEDERATION_PRIVKEY', $this->_private_part); + return $this; + } + + + public function privatePart() { + return $this->_private_part; + } + + + public function publicPart() { + return $this->_public_part; + } +} diff --git a/library/Class/Federation/GroupMembership.php b/library/Class/Federation/GroupMembership.php new file mode 100644 index 0000000000000000000000000000000000000000..e0bcf275a030294f87c743c02470d2235757050d --- /dev/null +++ b/library/Class/Federation/GroupMembership.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright (c) 2012-2019, 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_Federation_GroupMembershipLoader extends Storm_Model_Loader { + public function findAllReviewShare() { + return Class_Federation_GroupMembership::findAllBy(['group_name' => Class_Federation_GroupMembership::REVIEW_SHARE]); + } +} + + + +class Class_Federation_GroupMembership extends Storm_Model_Abstract { + use Trait_TimeSource, Trait_Translator; + + const + REVIEW_DISPLAY = 'REVIEW_DISPLAY', + REVIEW_SHARE = 'REVIEW_SHARE'; + + protected $_table_name = 'federation_group_membership'; + protected $_loader_class = 'Class_Federation_GroupMembershipLoader'; + + public function beforeSave() { + if ($this->isNew()) + $this->setAcceptedAt($this->getCurrentDateTime()); + } + + + public function afterDelete() { + if (static::REVIEW_SHARE == $this->getGroupName()) { + Class_AvisNotice::deleteBy(['source_actor_id' => $this->getActorId()]); + Class_Journal::deleteBy(['type' => $this->getHarvestJournalType()]); + } + } + + + public function validate() { + $this->checkAttribute('group_name', + in_array($this->getGroupName(), [static::REVIEW_DISPLAY, + static::REVIEW_SHARE]), + $this->_('Cannot join unknown group %s', $this->getGroupName())); + } + + + public function getHarvestJournalType() { + return 'AP_REVIEW_HARVEST_' . $this->getId(); + } +} diff --git a/library/Class/FederationReview.php b/library/Class/FederationReview.php new file mode 100644 index 0000000000000000000000000000000000000000..0c9eb4b368d63a10ee37f15f6383877cf66ee455 --- /dev/null +++ b/library/Class/FederationReview.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright (c) 2012-2019, 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_FederationReview { + use Trait_Singleton, Trait_Translator, Trait_LastMessage; + + const + FEATURE_SHARE = 'SHARE', + FEATURE_DISPLAY = 'DISPLAY'; + + + public function enableDisplay() { + return $this->_enableFeature(static::FEATURE_DISPLAY); + } + + + public function isDisplayEnabled() { + return $this->_isEnabled(static::FEATURE_DISPLAY); + } + + + public function disableDisplay() { + return $this->_disableFeature(static::FEATURE_DISPLAY); + } + + + public function enableShare() { + return $this->_enableFeature(static::FEATURE_SHARE); + } + + + public function isShareEnabled() { + return $this->_isEnabled(static::FEATURE_SHARE); + } + + + public function disableShare() { + return $this->_disableFeature(static::FEATURE_SHARE); + } + + + protected function _enableFeature($feature) { + if (!$this->_isKnownFeature($feature)) + return $this->_error($this->_('Impossible d\'activer une fonctionnalité inconnue')); + + if ($this->_isEnabled($feature)) + return true; + + if (!$community_server = $this->_getCommunityServer()) + return $this->_error($this->_('L\'adresse du serveur communautaire n\'est pas paramétrée')); + + $service = (new Class_WebService_ActivityPub($community_server)); + //->setLogger(new Zend_Log(new Zend_Log_Writer_Stream(PATH_TEMP . 'activitypub_client.log'))); + + if (!$service->isValid()) + return $this->_error($this->_('Le serveur communautaire paramétré n\'est pas compatible')); + + if (!$service->join('REVIEW_' . $feature )) + return $this->_error($this->_('Une erreur est survenue : %s', $service->getLastMessage())); + + Class_Journal::newInstance(['type' => 'AP_REVIEW_' . $feature . '_JOIN'])->save(); + return true; + } + + + protected function _isEnabled($feature) { + if (!$this->_isKnownFeature($feature)) + return false; + + $join = Class_Journal::lastOf('AP_REVIEW_' . $feature . '_JOIN'); + $leave = Class_Journal::lastOf('AP_REVIEW_' . $feature . '_LEAVE'); + + if (!$join) + return false; + + return $leave + ? $join->isAfter($leave) + : true; + } + + + protected function _disableFeature($feature) { + if (!$this->_isKnownFeature($feature)) + return $this->_error($this->_('Impossible de désactiver une fonctionnalité inconnue')); + + if (!$this->_isEnabled($feature)) + return; + + Class_Journal::newInstance(['type' => 'AP_REVIEW_' . $feature . '_LEAVE'])->save(); + + if (!$community_server = $this->_getCommunityServer()) + return; + + $service = new Class_WebService_ActivityPub($community_server); + if (!$service->isValid()) + return; + + $service->leave('REVIEW_' . $feature); + } + + + protected function _isKnownFeature($feature) { + return in_array($feature, [static::FEATURE_DISPLAY, static::FEATURE_SHARE]); + } + + + /** + * @param $record Class_Notice + * @param $page int + * @return Class_Notice_ReviewsSet + */ + public function getAvis($record, $page) { + if (!$this->isDisplayEnabled() || (!$service = $this->_getCommunityService())) + return Class_Notice_ReviewsSet::emptyInstance(); + + //$service->setLogger(new Zend_Log(new Zend_Log_Writer_Stream(PATH_TEMP . 'activitypub_client.log'))); + if (!$collection_page = $service->reviews($record, $page)) + return Class_Notice_ReviewsSet::emptyInstance(); + + $reviews = array_map([$this, '_receiveReview'], $collection_page->items()); + + return new Class_Notice_ReviewsSet($this->_('Avis communautaires'), + $reviews, + Class_AvisNotice::getNoteAverage($reviews), + $collection_page->totalItems(), + ceil($collection_page->totalItems()/5)); + } + + + protected function _receiveReview($review) { + unset($review['id']); + return (new Class_AvisNotice())->updateAttributes($review); + } + + + protected function _getCommunityService() { + if (!$community_server = $this->_getCommunityServer()) + return; + + $service = new Class_WebService_ActivityPub($community_server); + return $service->isValid() + ? $service + : null; + } + + + protected function _getCommunityServer() { + return Class_AdminVar::getValueOrDefault('FEDERATION_COMMUNITY_SERVER'); + } +} diff --git a/library/Class/HttpSignature.php b/library/Class/HttpSignature.php new file mode 100644 index 0000000000000000000000000000000000000000..e42e25ef0ab3d7ed3d98256c4e351cb44ad894bd --- /dev/null +++ b/library/Class/HttpSignature.php @@ -0,0 +1,194 @@ +<?php +/** + * Copyright (c) 2012-2019, 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 'library/phpseclib/autoload.php'; + + +class Class_HttpSignature extends Class_Entity { + const REQUEST_TARGET = '(request-target)'; + + protected $_logger; + + public function setLogger($logger) { + $this->_logger = $logger; + return $this; + } + + + protected function _log($message) { + if ($this->_logger) + $this->_logger->info($message); + + return $this; + } + + + public function __construct($content) { + $map = ['keyId', 'signature', 'headers', 'algorithm']; + + $parts = explode(',', $content); + foreach($parts as $part) { + if (!preg_match('/([^=]+)="([^"]+)"/', $part, $matches)) + continue; + + if (!in_array(trim($matches[1]), $map)) + continue; + + $this->set(ucfirst(trim($matches[1])), trim($matches[2])); + } + } + + + public function getHeaders() { + $headers = $this->get('Headers', ''); + if (static::REQUEST_TARGET != substr($headers, 0, strlen(static::REQUEST_TARGET))) + $headers = static::REQUEST_TARGET . ' ' . $headers; + return $headers; + } + + + public function verify($headers, $key) { + if ('hmac' == substr($this->getAlgorithm(), 0, 4)) + return $this->_verifyHmac($headers, $key); + + if ('rsa' == substr($this->getAlgorithm(), 0, 3)) + return $this->_verifyRsa($headers, $key); + } + + + protected function _verifyRsa($headers, $key) { + $rsa = new \phpseclib\Crypt\RSA(); + $rsa->loadKey($key); + $this->_log(sprintf('Will verify rsa : %s', $this->_messageFrom($headers, $this->getHeaders()))); + return @$rsa->verify($this->_messageFrom($headers, $this->getHeaders()), + base64_decode($this->getSignature())); + } + + + protected function _verifyHmac($headers, $key) { + return hash_equals($this->getSignature(), + $this->sign($headers, $this->getHeaders(), $key, $this->getAlgorithm())); + } + + + public function signResponseTo($request, $headers, $key) { + $signature = $this->sign($this->injectRequestTargetIn($request, $headers), + $this->getHeaders(), + $key, + $this->getAlgorithm()); + return $this->_assemble($signature); + } + + + public function signRequest($headers, $key) { + $signature = $this->sign($headers, $this->getHeaders(), $key, $this->getAlgorithm()); + return $this->_assemble($signature); + } + + + protected function _assemble($signature) { + $signature = ['keyId="' . Class_Url::absolute(['module' => 'activitypub', + 'controller' => 'review', + 'action' => 'pubkey'], null, true) . '"', + 'algorithm="'. $this->getAlgorithm() . '"', + 'headers="' . $this->getHeaders() . '"', + 'signature="' . $signature . '"']; + + return implode(', ', $signature); + } + + + public function sign($headers, $headers_to_sign, $key, $algorithm) { + if ('hmac' == substr($algorithm, 0, 4)) + return $this->_signHmac($headers, $headers_to_sign, $key, substr($algorithm, 5)); + + if ('rsa' == substr($algorithm, 0, 3)) + return $this->_signRsa($headers, $headers_to_sign, $key); + } + + + protected function _signHmac($headers, $headers_to_sign, $key, $algorithm) { + return base64_encode(hash_hmac($algorithm, + $this->_messageFrom($headers, $headers_to_sign), $key, true)); + } + + + protected function _signRsa($headers, $headers_to_sign, $key) { + $rsa = new \phpseclib\Crypt\RSA(); + $rsa->loadKey($key); + $this->_log(sprintf('Will sign rsa : %s', $this->_messageFrom($headers, $headers_to_sign))); + return base64_encode($rsa->sign($this->_messageFrom($headers, $headers_to_sign))); + } + + + protected function _messageFrom($headers, $headers_to_sign) { + $headers_to_sign = str_replace(static::REQUEST_TARGET, '', $headers_to_sign); + $parts = array_filter(explode(' ', $headers_to_sign)); + + /* $this->_log('Headers to sign: ' . json_encode($parts, JSON_PRETTY_PRINT)); */ + /* $this->_log('Headers: ' . json_encode($headers, JSON_PRETTY_PRINT)); */ + + $datas = [ $this->_getHeader($headers, static::REQUEST_TARGET) ]; + + foreach($parts as $part) { + if (!$part) + continue; + + if ($data = $this->_getHeader($headers, $part)) + $datas[] = $data; + } + + //$this->_log('Datas: ' . json_encode($datas, JSON_PRETTY_PRINT)); + + return implode("\n", $datas); + } + + + protected function _getHeader($headers, $header) { + $headers = array_change_key_case($headers); + $data = array_key_exists($header, $headers) + ? trim($headers[$header]) + : ''; + + return strtolower($header) . ': ' . $data; + } + + + public function injectRequestHeadersIn($request, $datas) { + foreach(explode(' ', $this->getHeaders()) as $header) { + if (!$header || static::REQUEST_TARGET == $header) + continue; + + $datas[$header] = $request->getHeader($header); + } + + return $this->injectRequestTargetIn($request, $datas); + } + + + public function injectRequestTargetIn($request, $datas) { + $datas[static::REQUEST_TARGET] = strtolower($request->getMethod()) + . ' ' + . $request->getBaseUrl() . $request->getPathInfo(); + + return $datas; + } +} diff --git a/library/Class/Journal.php b/library/Class/Journal.php new file mode 100644 index 0000000000000000000000000000000000000000..41f1bd939feed2dde70d13abeb85037dcf7d16a6 --- /dev/null +++ b/library/Class/Journal.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright (c) 2012-2017, 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_JournalLoader extends Storm_Model_Loader { + protected $_current_journal; + + public function factory($type, $model=null) { + $journal = Class_Journal::newInstance(['type' => $type]); + if (!$journal->save()) + return; + + if (!$model) + return $journal; + + $this->_current_journal = $journal; + $model->acceptJournalVisitor($this); + $this->_current_journal = null; + + return $journal; + } + + + public function visitDetail($type, $value) { + Class_JournalDetail::newInstance(['type' => $type, + 'value' => $value, + 'journal' => $this->_current_journal]) + ->save(); + } + + + public function lastOf($type) { + return Class_Journal::findFirstBy(['type' => $type, 'order' => 'created_at desc']); + } +} + + + +class Class_Journal extends Storm_Model_Abstract { + use Trait_TimeSource; + + protected $_table_name = 'journal'; + protected $_loader_class = 'Class_JournalLoader'; + protected $_has_many = ['details' => ['model' => 'Class_JournalDetail', + 'role' => 'journal', + 'dependents' => 'delete']]; + protected $_default_attribute_values = ['type' => '', + 'created_at' => '']; + + public function beforeSave() { + if ($this->isNew()) + $this->setCreatedAt($this->getTimeSource()->dateDayAndHours()); + } + + + public function getLibelle() { + return $this->getType() . ' ' . $this->getCreatedAt(); + } + + + public function getDateTime() { + return new DateTime($this->getCreatedAt()); + } + + + public function isAfter($other) { + return $other + ? $this->getDateTime() > $other->getDateTime() + : true; + } +} diff --git a/library/Class/JournalDetail.php b/library/Class/JournalDetail.php new file mode 100644 index 0000000000000000000000000000000000000000..5d94028844919000ca703c6f66b61cc9babd2dbc --- /dev/null +++ b/library/Class/JournalDetail.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright (c) 2012-2017, 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_JournalDetail extends Storm_Model_Abstract { + use Trait_TimeSource; + + protected $_table_name = 'journal_detail'; + protected $_belongs_to = ['journal' => ['model' => 'Class_Journal']]; + + protected $_default_attribute_values = ['type' => '', + 'value' => '']; + + +} diff --git a/library/Class/Notice.php b/library/Class/Notice.php index a8c434e3e06749745d77bdc0d0ab0fa4b6ca0425..db1ff67bb7fd37488c159b1ff1b6fb634e5119b6 100644 --- a/library/Class/Notice.php +++ b/library/Class/Notice.php @@ -241,7 +241,7 @@ class Class_Notice extends Storm_Model_Abstract { } - public function getAvisByUser($user) { + public function getAvisByUser($user) { return Class_AvisNotice::findAllBy(['clef_oeuvre' => $this->getClefOeuvre(), 'id_user' => $user->getId()]); } @@ -257,13 +257,22 @@ class Class_Notice extends Storm_Model_Abstract { } + public function getLocalAvis() { + if (!isset($this->_local_avis)) + $this->_local_avis = Class_AvisNotice::findAllBy(['clef_oeuvre' => $this->getClefOeuvre(), + 'source_actor_id' => null]); + + return $this->_local_avis; + } + + public function getAvisBibliothecaire() { - return Class_AvisNotice::filterByBibliothecaire($this->getAvis()); + return Class_AvisNotice::filterByBibliothecaire($this->getLocalAvis()); } public function getAvisAbonne() { - return Class_AvisNotice::filterByAbonne($this->getAvis()); + return Class_AvisNotice::filterByAbonne($this->getLocalAvis()); } @@ -369,19 +378,27 @@ class Class_Notice extends Storm_Model_Abstract { public function getAllAvisPerSource($page = null) { - $all_avis = array('bib' => array('liste' => $avis_bib = $this->getAvisBibliothecaires(), - 'note' => $this->getNoteMoyenneAvisBibliothecaires(), - 'nombre' => count($avis_bib), - 'titre' => 'Bibliothécaires'), - 'abonne' => array('liste' => $avis_abon = $this->getAvisAbonnes(), - 'note' => $this->getNoteMoyenneAvisAbonnes(), - 'nombre' => count($avis_abon), - 'titre' => 'Lecteurs du portail')); - - foreach (array('Class_WebService_Babelio', 'Class_WebService_Amazon') as $provider_class) { + $avis_bib = $this->getAvisBibliothecaires(); + $avis_abon = $this->getAvisAbonnes(); + + $all_avis = ['bib' => new Class_Notice_ReviewsSet($this->_('Bibliothécaires'), + $avis_bib, + $this->getNoteMoyenneAvisBibliothecaires(), + count($avis_bib)), + + 'abonne' => new Class_Notice_ReviewsSet($this->_('Lecteurs du portail'), + $avis_abon, + $this->getNoteMoyenneAvisAbonnes(), + count($avis_abon)), + ]; + + foreach (['Class_WebService_Babelio', 'Class_WebService_Amazon', 'Class_FederationReview'] + as $provider_class) { $provider = new $provider_class(); $source = strtolower(array_last(explode('_', $provider_class))); - if ($data = $provider->getAvis($this, $page)) $all_avis[$source] = $data; + $reviews_set = $provider->getAvis($this, $page); + if (!$reviews_set->isEmpty()) + $all_avis[$source] = $reviews_set; } return $all_avis; @@ -389,7 +406,7 @@ class Class_Notice extends Storm_Model_Abstract { public function getAvisBibliothecaires() { - return Class_AvisNotice::filterByBibliothecaire($this->getAvis()); + return Class_AvisNotice::filterByBibliothecaire($this->getLocalAvis()); } @@ -399,7 +416,7 @@ class Class_Notice extends Storm_Model_Abstract { public function getAvisAbonnes() { - return Class_AvisNotice::filterByAbonne($this->getAvis()); + return Class_AvisNotice::filterByAbonne($this->getLocalAvis()); } diff --git a/library/Class/Notice/ReviewsSet.php b/library/Class/Notice/ReviewsSet.php new file mode 100644 index 0000000000000000000000000000000000000000..c20973ff75afec36c8d7e2abf3f2ed7cb26cc1e9 --- /dev/null +++ b/library/Class/Notice/ReviewsSet.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright (c) 2012-2019, 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_Notice_ReviewsSet { + protected + $_reviews = [], + $_rating = 0, + $_count = 0, + $_page_count = 0, + $_label = ''; + + public static function emptyInstance() { + return new static('', [], 0, 0, 0, 0); + } + + + public function __construct($label, $reviews, $rating, $count, $page_count=0) { + $this->_label = $label; + $this->_reviews = $reviews; + $this->_rating = $rating; + $this->_count = $count; + $this->_page_count = $page_count; + } + + + public function isEmpty() { + return 0 == $this->_count; + } + + + public function hasPages() { + return !$this->isEmpty() && 0 < $this->_page_count; + } + + + public function getReviews() { + return $this->_reviews; + } + + + public function getRating() { + return $this->_rating; + } + + + public function getCount() { + return $this->_count; + } + + + public function getLabel() { + return $this->_label; + } +} diff --git a/library/Class/TimeSource.php b/library/Class/TimeSource.php index 2ebc5bd9421a4cb71f76e1ae233f978496457600..fa7bfa2bd640dcb8d1907f91015f657e44c1d3a2 100644 --- a/library/Class/TimeSource.php +++ b/library/Class/TimeSource.php @@ -52,6 +52,11 @@ class Class_TimeSource { } + public function dateHttpHeader() { + return gmdate('D, d M Y H:i:s \G\M\T', $this->time()); + } + + public function date() { $time = $this->time(); return $this->midnightTime(date('n', $time), date('j', $time), date('Y', $time)); diff --git a/library/Class/WebService/ActivityPub.php b/library/Class/WebService/ActivityPub.php new file mode 100644 index 0000000000000000000000000000000000000000..2f85af98942b68d448cae24db30a456ed9cc8e7a --- /dev/null +++ b/library/Class/WebService/ActivityPub.php @@ -0,0 +1,414 @@ +<?php +/** + * Copyright (c) 2012-2017, 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 __DIR__ . '/../../activitystreams/autoload.php'; + +use + Patbator\ActivityStreams\Model\Base, + Patbator\ActivityStreams\Model\Factory, + Patbator\ActivityStreams\Model\Service, + Patbator\ActivityStreams\Model\Accept, + Patbator\ActivityStreams\Model\Reject, + Patbator\ActivityStreams\Model\Group, + Patbator\ActivityStreams\Model\Join, + Patbator\ActivityStreams\Model\Leave, + Patbator\ActivityStreams\Model\CollectionPage, + Patbator\ActivityStreams\Stream; + + +class Class_WebService_ActivityPub { + use Trait_SimpleWebClient, Trait_TimeSource, Trait_LastMessage, Trait_Translator; + + const + MIME_TYPE = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + EMPTY_DIGEST = 'ZDQxZDhjZDk4ZjAwYjIwNGU5ODAwOTk4ZWNmODQyN2U='; + + protected static + $_throw_errors = false, + $_signer; + + protected + $_endpoint, + $_identity_cache, + $_logger; + + + /** @category testing */ + public static function setThrowErrors($flag) { + static::$_throw_errors = (bool)$flag; + } + + + /** @category testing */ + public static function setSigner($signer) { + static::$_signer = $signer; + } + + + protected function _signerFor($signature) { + if (static::$_signer) + return static::$_signer; + + return (new Class_HttpSignature($signature)) + ->setLogger($this->_logger); + } + + + public static function identityOf($actor) { + if (!$actor || !$actor->id()) + return; + + return (new static($actor->id()))->identify(); + } + + + public static function publicKeyOf($actor) { + if (!$actor || !$actor->publicKey()) + return; + + return (new static($actor->publicKey()))->identify(); + } + + + public function __construct($endpoint) { + $this->_endpoint = (substr($endpoint, -1) == '/' + ? substr($endpoint, 0, strlen($endpoint) - 1) + : $endpoint); + + Base::setFactory((new Factory) + ->mapTypeToClass('Service', 'Class_ActivityPub_Service') + ->mapTypeToClass('Key', 'Class_ActivityPub_PublicKey')); + } + + + public function setLogger($logger) { + $this->_logger = $logger; + return $this; + } + + + protected function _log($message) { + if ($this->_logger) + $this->_logger->info($message); + + return $this; + } + + + public function isValid() { + return (bool) $this->_inboxUrl(); + } + + + public function identify() { + if (($stream = Stream::fromJson($this->_identity())) + && ($object = $stream->getRoot())) + return $object; + + $this->_log('Cannot fetch valid identity from ' . $this->_endpoint); + } + + + protected function _identity() { + if ($this->_identity_cache) + return $this->_identity_cache; + + try { + return $this->_identity_cache = $this + ->getWebClient() + ->open_url($this->_endpoint, ['headers' => ['Accept' => static::MIME_TYPE]]); + } catch(Exception $e) { + $this->_log($e->getMessage()); + if (static::$_throw_errors) + throw $e; + } + } + + + protected function _inboxUrl() { + return (($service = $this->identify()) + && ($service instanceof Service)) + ? $service->inbox() + : null; + } + + + public function name() { + return (($service = $this->identify()) + && ($service instanceof Service)) + ? $service->name() + : null; + } + + + /** + * @param $group_name string + * @return bool + * + * Join a group by its name + * Return true on Accept or false on error or Reject + * Rejection reason can be retreived with $this->getLastMessage() + */ + public function join($group_name) { + return ($group_name = trim($group_name)) + ? $this->_postActivityWithValidation($this->_groupActivityWith(new Join(), $group_name)) + : $this->_error($this->_('Impossible de rejoindre un groupe vide')); + } + + + /** + * @param $group_name string + * @return bool + * + * Leave a group by its name + * Return true on Accept or false on error or Reject + * Rejection reason can be retreived with $this->getLastMessage() + */ + public function leave($group_name) { + return ($group_name = trim($group_name)) + ? $this->_postActivityWithValidation($this->_groupActivityWith(new Leave(), $group_name)) + : $this->_error($this->_('Impossible de quitter un groupe vide')); + } + + + protected function _groupActivityWith($activity, $group_name) { + return $activity + ->actor((new Service())->name((new Class_Federation)->getActorName()) + ->id(Class_Url::absolute(['module' => 'activitypub', + 'controller' => 'review'], null, true))) + ->object((new Group())->name($group_name)); + } + + + protected function _postActivityWithValidation($activity) { + if (!$inbox = $this->_inboxUrl()) + return $this->_error($this->_('Serveur invalide')); + + $request_target = 'post ' . Zend_Uri::factory($inbox)->getPath(); + + $response = $this->_activityPost($inbox, (new Stream($activity))->render(), $request_target); + $this->_log('Received response with headers : ' . json_encode($response->getHeaders(), JSON_PRETTY_PRINT)); + if (!$response_body = $this->_validateResponseAndBody($response, $request_target)) + return false; + + if ((!$stream = Stream::fromJson($response_body)) || (!$response_activity = $stream->getRoot())) + return $this->_error($this->_('La réponse du serveur n\'était pas valide')); + + if ($response_activity instanceof Accept) + return true; + + $message = $this->_('Le serveur a refusé votre demande'); + if (($response_activity instanceof Reject) + && ($summary = $response_activity->summary())) + $message .= ': ' . $summary; + + return $this->_error($message); + } + + + public function reviews($notice, $page) { + return $this->_getReviewsPage(['key' => $notice->getClefOeuvre(), + 'page' => $page]); + } + + + public function harvestReviews($from) { + $reviews = new Storm_Collection(); + $page = 1; + while (($collection_page = $this->_getReviewsPage(['from' => $from, + 'page' => $page])) + && ($items = $collection_page->items())) { + $reviews->addAll($items); + $page++; + } + + return $reviews; + } + + + protected function _getReviewsPage($query_params) { + if (!$outbox = $this->_outboxUrl()) + return; + + $outbox .= '?' . http_build_query($query_params); + $request_target = 'get ' . Zend_Uri::factory($outbox)->getPath(); + + $response = $this->_activityGet($outbox, $request_target); + + if (!$body = $this->_validateResponseAndBody($response, $request_target)) + return; + + if ((!$stream = Stream::fromJson($body)) || (!$activity = $stream->getRoot())) + return; + + return $activity instanceof CollectionPage + ? $activity + : null; + } + + + protected function _activityGet($url, $request_target) { + $headers = ['date' => $this->getTimeSource()->dateHttpHeader(), + 'digest' => 'MD5=' . static::EMPTY_DIGEST, + Class_HttpSignature::REQUEST_TARGET => $request_target]; + + return $this + ->getWebClient() + ->getResponse($url, + ['headers' => ['Date' => $headers['date'], + 'Digest' => $headers['digest'], + 'Accept' => static::MIME_TYPE, + 'Signature' => $this->_sign($headers), + 'Authorization' => 'Bearer ' . Class_Url::absolute(['module' => 'activitypub', + 'controller' => 'review'], null, true)]]); + } + + + protected function _outboxUrl() { + return (($service = $this->identify()) + && ($service instanceof Service)) + ? $service->outbox() + : null; + } + + + protected function _activityPost($url, $content, $request_target) { + $headers = ['date' => $this->getTimeSource()->dateHttpHeader(), + 'digest' => 'MD5=' . base64_encode(md5($content)), + Class_HttpSignature::REQUEST_TARGET => $request_target]; + + return $this + ->getWebClient() + ->postRawDataResponse($url, $content, static::MIME_TYPE, + ['headers' => ['Date' => $headers['date'], + 'Digest' => $headers['digest'], + 'Signature' => $this->_sign($headers)]]); + } + + + public function validateRequest($request) { + if (!$signature = $request->getHeader('Signature')) { + $this->_log('Request without Signature header'); + return false; + } + $this->_log('Received request with signature : ' . $signature); + + if (!$actor = $this->identify()) + return false; + + if (!$actor_key = $actor->publicKey()) { + $this->_log('Cannot verify signature without publickey of ' . $this->_endpoint); + return false; + } + + $signer = $this->_signerFor($signature); + if ($actor_key != $signer->getKeyId()) { + $this->_log(sprintf('Signature keyId differs from actor keyId : %s <---> %s', + $signer->getKeyId(), $actor_key)); + return false; + } + + if (!$key = (new static($actor_key))->setLogger($this->_logger)->identify()) { + $this->_log('Cannot get actor key details from ' . $actor_key); + return false; + } + + if ($key->owner() != $actor->id()) { + $this->_log(sprintf('Key owner is not actor : %s <---> %s', + $key->owner(), $actor->id())); + return false; + } + + $headers = $signer->injectRequestHeadersIn($request, []); + if (!$signer->verify($headers, $key->publicKeyPem())) { + $this->_log(sprintf('Invalid signature : %s does not valid %s', + $key->publicKeyPem(), $signature)); + return false; + } + + return true; + } + + + protected function _validateResponseAndBody($response, $request_target) { + if (!$this->_validateResponse($response, $request_target)) + return false; + + if (!$body = $response->getBody()) + return $this->_error($this->_('La réponse du serveur était vide')); + + return $body; + } + + + protected function _validateResponse($response, $request_target) { + if ($response->isError()) + return $this->_error($response->getMessage()); + + if (!$signature = $response->getHeader('Signature')) + return $this->_error($this->_('Le serveur n\'a pas signé sa réponse')); + + if (!$identity = $this->identify()) + return $this->_error($this->_('Le serveur ne fournit pas son identité')); + + $signer = $this->_signerFor($signature); + if ($identity->publicKey() != $signer->getKeyId()) + return $this->_error($this->_('La signature de la réponse n\'est pas la signature du serveur')); + + $headers = $response->getHeaders(); + $headers[Class_HttpSignature::REQUEST_TARGET] = $request_target; + + if (!$key = (new static($identity->publicKey()))->setLogger($this->_logger)->identify()) { + $this->_log('Cannot get actor key details from ' . $identity->publicKey()); + return $this->_error($this->_('Impossible de récupérer les détails de la clé publique du serveur')); + } + + if ($key->owner() != $identity->id()) { + $this->_log(sprintf('Key owner is not actor : %s <---> %s', + $key->owner(), $identity->id())); + return $this->_error($this->_('Le serveur n\'est pas le propriétaire de la clé qu\'il fourni')); + } + + if (!$signer->verify($headers, $key->publicKeyPem())) { + $this->_log(sprintf('Invalid signature : %s does not valid %s', + $key->publicKeyPem(), $signature)); + return $this->_error($this->_('La signature ne semble pas provenir du serveur')); + } + + return true; + } + + + protected function _sign($headers) { + return $this->_signerFor('algorithm="rsa-sha256", headers="date digest"') + ->signRequest($headers, $this->_privateKey()); + } + + + protected function _privateKey() { + return (new Class_Federation())->getPrivateKey(); + } + + + public function getEndpoint() { + return $this->_endpoint; + } +} diff --git a/library/Class/WebService/ActivityPubServer.php b/library/Class/WebService/ActivityPubServer.php new file mode 100644 index 0000000000000000000000000000000000000000..09c38cbd5142ef46de2b77e9960a6bd3d2d72ef5 --- /dev/null +++ b/library/Class/WebService/ActivityPubServer.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright (c) 2012-2017, 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 __DIR__ . '/../../activitystreams/autoload.php'; + +use Patbator\ActivityStreams\Stream; + + +class Class_WebService_ActivityPubServer extends Class_WebService_ActivityPub { + public function respondTo($request, $response, $activity) { + $body = (new Stream($activity))->render(); + $digest = 'MD5=' . base64_encode(md5($body)); + $signer = $this->_signerFor('algorithm="rsa-sha256", headers="digest"'); + + $response + ->setHeader('Content-Type', static::MIME_TYPE, true) + ->setHeader('Digest', $digest) + ->setHeader('Signature', + $signer->signResponseTo($request, ['digest' => $digest], $this->_privateKey())) + ->setBody($body) + ; + } +} diff --git a/library/Class/WebService/Amazon.php b/library/Class/WebService/Amazon.php index 3a5d4e1267fdfd829c284a099e995ad52ebb7368..9e8a52463585dca9362aa40ff0d740784d7bdf7c 100644 --- a/library/Class/WebService/Amazon.php +++ b/library/Class/WebService/Amazon.php @@ -18,33 +18,24 @@ * along with BOKEH; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -////////////////////////////////////////////////////////////////////////////////////////// -// OPAC3 - WEB-SERVICE AMAZON -////////////////////////////////////////////////////////////////////////////////////////// -class Class_WebService_Amazon -{ - var $xml; // Pointeur sur la classe xml de base - private $req; // Racine requete http - private $id_afi="AKIAINZSICEPECFZ4RPQ"; // ID afi chez amazon dans cfg - private $secret_key="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"; // Clé secrete chez amazon -//------------------------------------------------------------------------------------------------------ -// Constructeur -//------------------------------------------------------------------------------------------------------ - function __construct() - { +class Class_WebService_Amazon { + use Trait_Translator; + + public $xml; + private $req; + private $id_afi="AKIAINZSICEPECFZ4RPQ"; + private $secret_key="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"; + + public function __construct() { $this->xml= new Class_Xml(); $this->req="http://webservices.amazon.fr/onca/xml?Service=AWSECommerceService"; $this->req.="&AWSAccessKeyId=".$this->id_afi; } -//------------------------------------------------------------------------------------------------------ -// Execution requete http et test erreur -//------------------------------------------------------------------------------------------------------ - function requete($req) - { + public function requete($req) { $url=$this->req.$req; // Ajout de signature AMAZON @@ -68,11 +59,8 @@ class Class_WebService_Amazon return $this->test_erreur(); } -//------------------------------------------------------------------------------------------------------ -// Retourne la notice d'après un noeud de type item -//------------------------------------------------------------------------------------------------------ - function rend_notice($node) - { + + public function rend_notice($node) { $notice["asin"]=$this->xml->get_child_value($node,"asin"); $img=$this->xml->get_child_node($node,"smallimage"); if($img) @@ -106,48 +94,42 @@ class Class_WebService_Amazon } - // pour rendre le service polymorphique avec Babelio, Amazon, Notices .... public function getAvis($notice, $page) { - if (! $notice->isLivre()) return false; - $avis = $this->rend_avis($notice, $page); - if ($avis == false) return false; - - $avis['titre'] = 'Lecteurs Amazon'; - return $avis; + return $notice->isLivre() + ? $this->rend_avis($notice, $page) + : Class_Notice_ReviewsSet::emptyInstance(); } -//------------------------------------------------------------------------------------------------------ -// Avis des lecteurs -//------------------------------------------------------------------------------------------------------ - function rend_avis($notice, $page) { - if ($notice instanceof Class_Notice) - $isbn = $notice->getIsbn(); - else - $isbn = $notice; - - if(!trim($isbn)){ - return false; - } - if($page>0){ + + public function rend_avis($notice, $page) { + $isbn = $notice instanceof Class_Notice + ? $notice->getIsbn() + : $notice; + + if (!trim($isbn)) + return Class_Notice_ReviewsSet::emptyInstance(); + + if ($page > 0) $page="&ReviewPage=".$page; - } - $req=$this->req_isbn($isbn)."&ResponseGroup=Reviews".$page; - if(!$this->requete($req)){ - return false; - } - $item=$this->xml->getNode("customerreviews"); - $avis["note"]=$this->xml->get_child_value($item,"averagerating"); - if(!$avis["note"]){ - return false; - } - $avis["nombre"]=$this->xml->get_child_value($item,"totalreviews"); - $avis["nb_pages"]=$this->xml->get_child_value($item,"totalreviewpages"); - $item=$this->xml->get_child_node($item,"review"); + $req = $this->req_isbn($isbn) . "&ResponseGroup=Reviews" . $page; + if (!$this->requete($req)) + return Class_Notice_ReviewsSet::emptyInstance(); + + $item = $this->xml->getNode("customerreviews"); + $rating = $this->xml->get_child_value($item, "averagerating"); + if (!$rating) + return Class_Notice_ReviewsSet::emptyInstance(); + + $count = $this->xml->get_child_value($item, "totalreviews"); + $page_count = $this->xml->get_child_value($item, "totalreviewpages"); + + $item = $this->xml->get_child_node($item, "review"); $dateClass = new Class_Date(); - while( $item ) { - $avis_notice = new Class_AvisNotice(); - $avis_notice + $reviews = []; + + while($item) { + $reviews[] = (new Class_AvisNotice()) ->setNote($this->xml->get_child_value($item,"rating")) ->setDateAvis($dateClass->LocalizedDate($this->xml->get_child_value($item,"date"), 'yyyy-MM-dd')) ->setEntete(utf8_encode($this->xml->get_child_value($item,"summary"))) @@ -155,17 +137,17 @@ class Class_WebService_Amazon ->setNotice($notice) ->setUser(null); - $index=count($avis["liste"]); - $avis["liste"][$index] = $avis_notice; - $item=$this->xml->get_sibling($item); + $item = $this->xml->get_sibling($item); } - return $avis; + return new Class_Notice_ReviewsSet($this->_('Lecteurs Amazon'), + $reviews, + $rating, + $count, + $page_count); } -//------------------------------------------------------------------------------------------------------ -// Résumés et analyses -//------------------------------------------------------------------------------------------------------ + public function getResumes($notice) { if (!$service = $notice->getIsbnOrEan()) return array(); @@ -174,7 +156,7 @@ class Class_WebService_Amazon } - function rend_analyses($isbn) { + public function rend_analyses($isbn) { if(!trim($isbn)) return array(); diff --git a/library/Class/WebService/Babelio.php b/library/Class/WebService/Babelio.php index 993d271fa245f87c929baa114154a775a03ef0a3..e51bf47d370e12aa2f65fc21e2f5becdae910f43 100644 --- a/library/Class/WebService/Babelio.php +++ b/library/Class/WebService/Babelio.php @@ -19,6 +19,8 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ class Class_WebService_Babelio { + use Trait_Translator; + const USER = 'afi_test'; const PASS = 'af_45_POQ_d'; @@ -130,18 +132,9 @@ class Class_WebService_Babelio { * @return mixed */ public function getAvis($notice, $page) { - if (! $notice->isLivre()) - return false; - - if (! $this->_serviceActivated()) - return false; - - $avis = $this->getCritiques($notice); - if ($avis == false) - return false; - - $avis['titre'] = 'Lecteurs Babelio'; - return $avis; + return $notice->isLivre() && $this->_serviceActivated() + ? $this->getCritiques($notice) + : Class_Notice_ReviewsSet::emptyInstance(); } @@ -162,7 +155,7 @@ class Class_WebService_Babelio { $isbn = $notice->getIsbn(); $this->requete($isbn); if (!$this->_xml) - return false; + return Class_Notice_ReviewsSet::emptyInstance(); $liste_avis = array(); foreach($this->_xml->url as $avis) { @@ -185,9 +178,10 @@ class Class_WebService_Babelio { $liste_avis[] = $avis_notice; } - return array("liste" => $liste_avis, - "nombre" => count($liste_avis), - "note" => Class_AvisNotice::getNoteAverage($liste_avis)); + return new Class_Notice_ReviewsSet($this->_('Lecteurs Babelio'), + $liste_avis, + Class_AvisNotice::getNoteAverage($liste_avis), + count($liste_avis)); } diff --git a/library/Class/WebService/SimpleWebClient.php b/library/Class/WebService/SimpleWebClient.php index 5ec8ffc117230e3f8539a530c83e639aef7172f5..247b85e1e34890da985d86465d969dc158518433 100644 --- a/library/Class/WebService/SimpleWebClient.php +++ b/library/Class/WebService/SimpleWebClient.php @@ -78,6 +78,12 @@ class Class_WebService_SimpleWebClient { public function postRawData($url, $datas, $encoding, $options = []) { + return $this->_postRawData($url, $datas, $encoding, $options) + ->getBody(); + } + + + protected function _postRawData($url, $datas, $encoding, $options) { $httpClient = $this->getHttpClient(); $httpClient->resetParameters(); $httpClient->setUri($url); @@ -87,7 +93,12 @@ class Class_WebService_SimpleWebClient { if (isset($options['headers'])) $httpClient->setHeaders($options['headers']); - return $httpClient->request()->getBody(); + return $httpClient->request(); + } + + + public function postRawDataResponse($url, $datas, $encoding, $options = []) { + return $this->_postRawData($url, $datas, $encoding, $options); } @@ -104,5 +115,3 @@ class Class_WebService_SimpleWebClient { : null; } } - -?> \ No newline at end of file diff --git a/library/Trait/LastMessage.php b/library/Trait/LastMessage.php new file mode 100644 index 0000000000000000000000000000000000000000..48b81119d0f8763cd49b49e724d02f30c2f3170d --- /dev/null +++ b/library/Trait/LastMessage.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright (c) 2012-2019, 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 Trait_LastMessage { + protected $_last_message = ''; + + public function getLastMessage() { + return $this->_last_message; + } + + + protected function _error($message) { + $this->_last_message = $message; + return false; + } +} diff --git a/library/ZendAfi/Acl/AdminControllerGroup.php b/library/ZendAfi/Acl/AdminControllerGroup.php index ba103b09e36bf4e0c5abc73459dd913a39f31729..08c2b62b14c1b0596df128a5ec8ca5e8507c79e4 100644 --- a/library/ZendAfi/Acl/AdminControllerGroup.php +++ b/library/ZendAfi/Acl/AdminControllerGroup.php @@ -85,6 +85,7 @@ class ZendAfi_Acl_AdminControllerGroup { 'custom-fields-report' => Class_AdminVar::isCustomFieldsReportEnabled(), 'usergroup-agenda' => Class_AdminVar::isRendezVousEnabled(), 'rendez-vous' => Class_AdminVar::isRendezVousEnabled(), + 'federation-reviews' => Class_AdminVar::isFederationEnabled(), ]; $this->_activated = array_merge($this->_activated, diff --git a/library/ZendAfi/Acl/AdminControllerRoles.php b/library/ZendAfi/Acl/AdminControllerRoles.php index 28e56a51fabf5231a8f54123941c3c32352294af..a891142242831ed029413c75e70e6ef875bf4a0d 100644 --- a/library/ZendAfi/Acl/AdminControllerRoles.php +++ b/library/ZendAfi/Acl/AdminControllerRoles.php @@ -100,6 +100,7 @@ class ZendAfi_Acl_AdminControllerRoles extends Zend_Acl { $this->add(new Zend_Acl_Resource('search-form')); $this->add(new Zend_Acl_Resource('usergroup-agenda')); $this->add(new Zend_Acl_Resource('rendez-vous')); + $this->add(new Zend_Acl_Resource('journal')); $codifications = ['codification-browser', 'thesauri', @@ -204,6 +205,7 @@ class ZendAfi_Acl_AdminControllerRoles extends Zend_Acl { $this->deny('modo_portail','systeme/phpinfo'); $this->deny('modo_portail','usergroup-agenda'); $this->deny('modo_portail','rendez-vous'); + $this->deny('modo_portail','journal'); foreach($codifications as $controller) $this->deny('modo_portail', $controller); diff --git a/library/ZendAfi/Controller/Action/Helper/Journal.php b/library/ZendAfi/Controller/Action/Helper/Journal.php new file mode 100644 index 0000000000000000000000000000000000000000..307bd958153ccdaabf6fa49863ce9967cd33237a --- /dev/null +++ b/library/ZendAfi/Controller/Action/Helper/Journal.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright (c) 2012-2017, 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_Controller_Action_Helper_Journal + extends Zend_Controller_Action_Helper_Abstract{ + + public function journal($type, $model=null) { + return Class_Journal::factory($type, $model); + } + + + public function direct($type, $model=null) { + return $this->journal($type, $model); + } +} diff --git a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Abstract.php b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Abstract.php index 6d983cca9ae77825ea10696b9d07260ca2323677..566dbe8e6e8bcc09218b33c11263b8c267504d1d 100644 --- a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Abstract.php +++ b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Abstract.php @@ -43,6 +43,12 @@ abstract class ZendAfi_Controller_Action_Helper_ListViewMode_Abstract } + protected function _initParams($params) { + $this->_params = $params; + return $this; + } + + public function getName() { return str_replace('ZendAfi_Controller_Action_Helper_', '', get_class($this)); } diff --git a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Codification/Flat.php b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Codification/Flat.php index 8f69470b496cbc17834c8ef895e8a960c36a4541..2c6c5d958b18bee3bcd764130f1ba4019b04225d 100644 --- a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Codification/Flat.php +++ b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Codification/Flat.php @@ -26,7 +26,7 @@ class ZendAfi_Controller_Action_Helper_ListViewMode_Codification_Flat protected $_model_class; protected function _initParams($params) { - $this->_params = $params; + parent::_initParams($params); $this->_model_class = get_class($this->getModel()); } diff --git a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Journal.php b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Journal.php new file mode 100644 index 0000000000000000000000000000000000000000..b58106630f71297f996a5c4ad058d3c2e1dbe980 --- /dev/null +++ b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Journal.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright (c) 2012-2017, 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_Controller_Action_Helper_ListViewMode_Journal + extends ZendAfi_Controller_Action_Helper_ListViewMode_Abstract { + + public function ListViewMode_Journal($params) { + return parent::_initParams($params); + } + + + public function direct($params) { + return $this->ListViewMode_Journal($params); + } + + + public function isSearchEnabled() { + return false; + } + + + protected function _describeCategoriesIn($description) { + return $description; + } + + + protected function _describeItemsIn($description) { + return $description + ->addColumn($this->_('Date'), 'created_at') + ->addColumn($this->_('Type'), 'type') + ->setSorterServer(); + } + + + public function getItems() { + $params = ['limitPage' => [$this->getPage(), $this->_items_by_page], + 'order' => $this->getOrder()]; + return Class_Journal::findAllBy($params); + } + + + protected function getOrder() { + return $this->getParam('order', 'created_at desc'); + } + + + protected function enabledSorter() { + return false; + } +} diff --git a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Library.php b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Library.php index 0427097d0e9d33cfbf0bee1678bf994304d4d830..373cd3f43f6de3d1a8372decb941ecf9bd0964b8 100644 --- a/library/ZendAfi/Controller/Action/Helper/ListViewMode/Library.php +++ b/library/ZendAfi/Controller/Action/Helper/ListViewMode/Library.php @@ -23,8 +23,7 @@ class ZendAfi_Controller_Action_Helper_ListViewMode_Library extends ZendAfi_Controller_Action_Helper_ListViewMode_Abstract { public function ListViewMode_Library($params) { - $this->_params = $params; - return $this; + return parent::_initParams($params); } diff --git a/library/ZendAfi/Controller/Plugin/AdminAuth.php b/library/ZendAfi/Controller/Plugin/AdminAuth.php index 871b912c036eeb26b836922f90947fe59766a943..7bd9242d2b366fa6a33e1a6c00f65fe8b097b193 100644 --- a/library/ZendAfi/Controller/Plugin/AdminAuth.php +++ b/library/ZendAfi/Controller/Plugin/AdminAuth.php @@ -46,6 +46,15 @@ class ZendAfi_Controller_Plugin_AdminAuth extends Zend_Controller_Plugin_Abstrac return; } + // activitypub + if ('activitypub' == $module + && !Class_AdminVar::isFederationServiceAvailable()) { + $request->setModuleName('activitypub') + ->setControllerName('review') + ->setActionName('unavailable'); + return; + } + // Entree dans opac on teste si le site a été désactivé if (Class_AdminVar::get("SITE_OK") == "0" and $module == 'opac') { $controller = 'index'; diff --git a/library/ZendAfi/Controller/Plugin/ResourceDefinition/Journal.php b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Journal.php new file mode 100644 index 0000000000000000000000000000000000000000..15f210c74c4c96901e62d88660178762b6c93029 --- /dev/null +++ b/library/ZendAfi/Controller/Plugin/ResourceDefinition/Journal.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright (c) 2012-2017, 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_Controller_Plugin_ResourceDefinition_Journal + extends ZendAfi_Controller_Plugin_ResourceDefinition_Abstract { + + public function getDefinitions() { + return + ['model' => ['class' => 'Class_Journal', + 'name' => 'journal', + 'order' => 'created_at desc'], + + 'listViewMode' => ['helper_method' => 'ListViewMode_Journal'], + + 'actions' => ['index' => ['title' => $this->_('Journal d\'évènements')]]]; + } +} \ No newline at end of file diff --git a/library/ZendAfi/Form/Admin/AdminVarFactory.php b/library/ZendAfi/Form/Admin/AdminVarFactory.php index 60faeae3124e217e761a539cb96db2f65fe979a1..8d45130554e8252c565c409a306c9d7cef57a59c 100644 --- a/library/ZendAfi/Form/Admin/AdminVarFactory.php +++ b/library/ZendAfi/Form/Admin/AdminVarFactory.php @@ -33,7 +33,8 @@ class ZendAfi_Form_Admin_AdminVarFactory { Class_AdminVar_Meta::TYPE_ENCODED_DATA => 'ZendAfi_Form_Admin_AdminVar_EncodedData', Class_AdminVar_Meta::TYPE_RAW_TEXT => 'ZendAfi_Form_Admin_AdminVar_RawText', Class_AdminVar_Meta::TYPE_COMBO => 'ZendAfi_Form_Admin_AdminVar_Combo', - Class_AdminVar_Meta::TYPE_EDITOR => 'ZendAfi_Form_Admin_AdminVar_Editor' + Class_AdminVar_Meta::TYPE_EDITOR => 'ZendAfi_Form_Admin_AdminVar_Editor', + Class_AdminVar_Meta::TYPE_CRYPT_KEY => 'ZendAfi_Form_Admin_AdminVar' ]; diff --git a/library/ZendAfi/Validate/ActivityPubEndpoint.php b/library/ZendAfi/Validate/ActivityPubEndpoint.php new file mode 100644 index 0000000000000000000000000000000000000000..cdc823f6a47481e345c2df5371cd6f6b1335e40c --- /dev/null +++ b/library/ZendAfi/Validate/ActivityPubEndpoint.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright (c) 2012-2017, 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_Validate_ActivityPubEndpoint extends Zend_Validate_Abstract { + const INVALID_VALUE = 'invalidValue'; + + protected $_messageTemplates = + [ + self::INVALID_VALUE => "'%value%' n'est pas un service de fédération valide." + ]; + + public function isValid($value) { + $this->_setValue((string)$value); + + $service = new Class_WebService_ActivityPub((string)$value); + if ($service->isValid()) + return true; + + $this->_error(static::INVALID_VALUE); + } +} diff --git a/library/ZendAfi/Validate/Url.php b/library/ZendAfi/Validate/Url.php index 8d20c40c254cd26701e3a2b1da88a5e97c5c8b50..eb56e38ff342304b2c1fcdc2985a25245d6c77da 100644 --- a/library/ZendAfi/Validate/Url.php +++ b/library/ZendAfi/Validate/Url.php @@ -18,6 +18,8 @@ * along with BOKEH; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + + class ZendAfi_Validate_Url extends Zend_Validate_Abstract { const INVALID_URL = 'invalidUrl'; @@ -39,4 +41,3 @@ class ZendAfi_Validate_Url extends Zend_Validate_Abstract { return true; } } -?> \ No newline at end of file diff --git a/library/ZendAfi/View/Helper/Admin/ContentNav.php b/library/ZendAfi/View/Helper/Admin/ContentNav.php index e763e735787452eab58df1d92cad33ef9c7b76ee..af75679b6b0e9d53de89b5842c5fcee0e5f11639 100644 --- a/library/ZendAfi/View/Helper/Admin/ContentNav.php +++ b/library/ZendAfi/View/Helper/Admin/ContentNav.php @@ -32,6 +32,7 @@ class ZendAfi_View_Helper_Admin_ContentNav extends ZendAfi_View_Helper_BaseHelpe $this->menuMiseEnPage(), $this->menuStats(), $this->menuPortail(), + $this->menuFederation(), $this->menuCatalogue(), $this->menuSysteme(), ]; @@ -131,6 +132,15 @@ class ZendAfi_View_Helper_Admin_ContentNav extends ZendAfi_View_Helper_BaseHelpe } + protected function menuFederation() { + return $this + ->renderBloc($this->_('Fédération'), + [ + ['frbr', $this->_('Avis'), '/admin/federation-reviews'], + ]); + } + + public function menuSysteme() { $is_admin = function($user) { return $user->isAdmin(); }; $is_super_admin = function($user) { return $user->isSuperAdmin(); }; @@ -160,7 +170,8 @@ class ZendAfi_View_Helper_Admin_ContentNav extends ZendAfi_View_Helper_BaseHelpe ['search_form', $this->_('Formulaires de recherche'), '/admin/search-form'], ['customfields', $this->_('Champs personnalisés'), '/admin/custom-fields/index', [], $is_admin], - ['customreports', $this->_('Rapports statistiques'), '/admin/custom-fields-report'] + ['customreports', $this->_('Rapports statistiques'), '/admin/custom-fields-report'], + ['variables', $this->_('Journal'), '/admin/journal'] ]); } diff --git a/library/ZendAfi/View/Helper/Admin/HelpLink.php b/library/ZendAfi/View/Helper/Admin/HelpLink.php index 4635bab7b5498c6c7dac8c17e27d514123f815cc..521275a5c37810be41524215d59cde2d143d03d3 100644 --- a/library/ZendAfi/View/Helper/Admin/HelpLink.php +++ b/library/ZendAfi/View/Helper/Admin/HelpLink.php @@ -123,6 +123,7 @@ class ZendAfi_View_Helper_Admin_HelpLinkBokehWiki { 'registration' => ['index' => 'Gérer_les_demandes_d\'inscription'], 'usergroup-agenda' => ['index' => 'Rendez-vous'], 'rendez-vous' => ['index' => 'Rendez-vous'], + 'federation-reviews' => ['index' => 'Avis_communautaires'], 'genre' => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'], 'emplacement' => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'], 'section' => ['index' => 'Codification_des_genres,_emplacements,_annexes,_...'], diff --git a/library/ZendAfi/View/Helper/Avis.php b/library/ZendAfi/View/Helper/Avis.php index d18e4b4e2926520cc0ddd7db1e53e182f77b5c4c..2cfe418a97a7aa306bd77b4567201cb772a24d49 100644 --- a/library/ZendAfi/View/Helper/Avis.php +++ b/library/ZendAfi/View/Helper/Avis.php @@ -170,6 +170,9 @@ class ZendAfi_View_Helper_Avis extends ZendAfi_View_Helper_BaseHelper { protected function _renderAuthor($avis) { + if ($avis->hasSourceAuthor()) + return $this->_renderFederationAuthor($avis); + $auteur = $this->view->escape($avis->getUserName()); $url_auteur = $this->_urlWithContext($this->_getUrlAuthor($avis)); @@ -186,6 +189,16 @@ class ZendAfi_View_Helper_Avis extends ZendAfi_View_Helper_BaseHelper { } + protected function _renderFederationAuthor($avis) { + $html = $avis->getSourceAuthor() + . ' ' + . $this->_tag('span', '- ' . $avis->getReadableDateAvis()); + + return $this->_tag('span', $html, + ['class' => 'auteur_critique']); + } + + protected function _getUrlAuthor($avis) { if ($avis->isAvisNotice()) return ['module' => 'opac', @@ -325,4 +338,4 @@ class ZendAfi_View_Helper_Avis extends ZendAfi_View_Helper_BaseHelper { return ['text_avis' => nl2br($content), 'lire_la_suite' => $read_link]; } -} \ No newline at end of file +} diff --git a/library/ZendAfi/View/Helper/Notice/Avis.php b/library/ZendAfi/View/Helper/Notice/Avis.php index 6af6890666640a4a5d81ddacc27d369b273b0553..0cb70c40746196de892a1282c86d2f59b7f4831f 100644 --- a/library/ZendAfi/View/Helper/Notice/Avis.php +++ b/library/ZendAfi/View/Helper/Notice/Avis.php @@ -18,9 +18,7 @@ * 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_Notice_Avis extends Zend_View_Helper_HtmlElement { - use Trait_Translator; - +class ZendAfi_View_Helper_Notice_Avis extends ZendAfi_View_Helper_BaseHelper { protected $_notice, $_avis, $_params; public function Notice_Avis($notice, $avis, $params) { @@ -31,8 +29,7 @@ class ZendAfi_View_Helper_Notice_Avis extends Zend_View_Helper_HtmlElement { $user = Class_Users::getIdentity(); return $this->_tag('table', - $this->_header($user) - . $this->_advices($user), + $this->_header($user) . $this->_advices($user), ['cellspacing' => 0, 'width' => '100%']); } @@ -45,43 +42,40 @@ class ZendAfi_View_Helper_Notice_Avis extends Zend_View_Helper_HtmlElement { if ($user && $user->isBibliothecaire()) $avis_helper->setAdminActions(['edit', 'del']); - $html = $this->_tag('tr', - $this->_tag('td', ' ', ['colspan' => 3])) - . $this->_tag('tr', - $this->_tag('td', $this->_avis[$source]["titre"], - ['class' => 'notice_info_ligne_titre', - 'align' => 'left', - 'colspan' => 3])); - - if (0 == $this->_avis[$source]['nombre']) { - return $html .= $this->_tag('tr', - $this->_tag('td', ' ', ['colspan' => 3])) - . $this->_tag('tr', - $this->_tag('td', $this->_('Aucun avis pour le moment'), - ['colspan' => 3, - 'class' => 'notice_info', - 'style' => 'text-align:left'])) - . $this->_tag('tr', - $this->_tag('td', ' ', ['colspan' => 3])); - } + $reviews_set = array_key_exists($source, $this->_avis) + ? $this->_avis[$source] + : Class_Notice_ReviewsSet::emptyInstance(); + + $html = $this->_tag('tr', $this->_tag('td', ' ', ['colspan' => 3])) + . $this->_tag('tr', $this->_tag('td', $reviews_set->getLabel(), + ['class' => 'notice_info_ligne_titre', + 'align' => 'left', + 'colspan' => 3])); - foreach($this->_avis[$source]["liste"] as $detail) { + if ($reviews_set->isEmpty()) + return $html .= $this->_tag('tr', $this->_tag('td', ' ', ['colspan' => 3])) + . $this->_tag('tr', $this->_tag('td', $this->_('Aucun avis pour le moment'), + ['colspan' => 3, + 'class' => 'notice_info', + 'style' => 'text-align:left'])) + . $this->_tag('tr', $this->_tag('td', ' ', ['colspan' => 3])); + + + foreach($reviews_set->getReviews() as $detail) { if ($detail->isVisibleForUser($user)) $html .= $this->_tag('tr', $this->_tag('td', $avis_helper->contenu_avis($detail), ['colspan' => 3])); } - if (isset($this->_avis[$source]["nb_pages"]) - && ($this->_avis[$source]["nb_pages"] > 1)) { + if ($reviews_set->hasPages()) { $pager = $this->view->getHelper('Pager'); - $lien = "javascript:".$fct."('".$this->_params["onglet"]."','".$this->_notice->getId()."','avis','".$source."',1,@PAGE@)"; - $urlPagesHtml = $pager->Pager($this->_avis[$source]["nombre"], 5, $this->_params["page"], $lien); - $html .= $this->_tag('tr', - $this->_tag('td', $urlPagesHtml, - ['colspan' => 3, - 'class' => 'notice_info', - 'style' => 'text-align:center'])); + $lien = "javascript:" . $this->_jsFunction() . "('".$this->_params["onglet"]."','".$this->_notice->getId()."','avis','".$source."',1,@PAGE@)"; + $urlPagesHtml = $pager->Pager($reviews_set->getCount(), 5, $this->_params["page"], $lien); + $html .= $this->_tag('tr', $this->_tag('td', $urlPagesHtml, + ['colspan' => 3, + 'class' => 'notice_info', + 'style' => 'text-align:center'])); } return $html; @@ -92,11 +86,12 @@ class ZendAfi_View_Helper_Notice_Avis extends Zend_View_Helper_HtmlElement { if (array_key_exists('cherche', $this->_params)) return $this->_params['cherche']; - if($this->_avis["bib"]["nombre"] > 0) + if (!$this->_avis['bib']->isEmpty()) return 'bib'; - if($this->_avis["abonne"]["nombre"] > 0) + if (!$this->_avis['abonne']->isEmpty()) return 'abonne'; + return null; } @@ -117,20 +112,15 @@ class ZendAfi_View_Helper_Notice_Avis extends Zend_View_Helper_HtmlElement { protected function _headerSources() { $html = ''; - foreach($this->_avis as $source => $ligne) { - $count = $this->_avis[$source]['nombre']; - if (0 == $count) + foreach($this->_avis as $key => $reviews_set) { + if ($reviews_set->isEmpty()) continue; - $fct = (isset($this->_params['onglet']) - && (substr($this->_params['onglet'], 0, 4)) == 'bloc') ? - 'infos_bloc' : 'infos_onglet'; - - $url_site = "javascript:".$fct."('" . $this->_params["onglet"]."','".$this->_notice->getId() . "','avis','" . $source . "',1,1)"; + $url_site = "javascript:" . $this->_jsFunction() . "('" . $this->_params["onglet"]."','".$this->_notice->getId() . "','avis','" . $key . "',1,1)"; - $html .= $this->_headerSource($this->_avis[$source]["titre"], - $this->_getAdviceCountLabel($count), - $ligne['note'], + $html .= $this->_headerSource($reviews_set->getLabel(), + $this->_getAdviceCountLabel($reviews_set->getCount()), + $reviews_set->getRating(), $url_site); } @@ -138,6 +128,14 @@ class ZendAfi_View_Helper_Notice_Avis extends Zend_View_Helper_HtmlElement { } + protected function _jsFunction() { + return (isset($this->_params['onglet']) + && (substr($this->_params['onglet'], 0, 4)) == 'bloc') + ? 'infos_bloc' + : 'infos_onglet'; + } + + protected function _headerSource($label, $count, $note, $url) { return $this->_tag('li', $this->view->NoteImg($note) . ' ' @@ -171,6 +169,7 @@ class ZendAfi_View_Helper_Notice_Avis extends Zend_View_Helper_HtmlElement { return ''; } + protected function getLink($id_notice) { return $this->view->tagAnchor($this->view->url(['controller' => 'noticeajax', 'action' => 'add-avis', @@ -179,10 +178,4 @@ class ZendAfi_View_Helper_Notice_Avis extends Zend_View_Helper_HtmlElement { ['class' => 'notice', 'data-popup' => 'true']); } - - - protected function _tag() { - return call_user_func_array([$this->view, 'tag'], func_get_args()); - } } -?> \ No newline at end of file diff --git a/library/activitystreams b/library/activitystreams new file mode 160000 index 0000000000000000000000000000000000000000..fb573517b032e10ad2630b7d1e2440b9e6e28a63 --- /dev/null +++ b/library/activitystreams @@ -0,0 +1 @@ +Subproject commit fb573517b032e10ad2630b7d1e2440b9e6e28a63 diff --git a/library/phpseclib b/library/phpseclib new file mode 160000 index 0000000000000000000000000000000000000000..bc6ac8edfdea41760743acedd8e431677ebef75d --- /dev/null +++ b/library/phpseclib @@ -0,0 +1 @@ +Subproject commit bc6ac8edfdea41760743acedd8e431677ebef75d diff --git a/tests/application/modules/AbstractControllerTestCase.php b/tests/application/modules/AbstractControllerTestCase.php index 2e9a9a47feffa686971862aeef2c1c5025f3bc59..40e8e4d11bcd9260581eb05964fc3cf98d48c31a 100644 --- a/tests/application/modules/AbstractControllerTestCase.php +++ b/tests/application/modules/AbstractControllerTestCase.php @@ -197,6 +197,17 @@ abstract class AbstractControllerTestCase extends Zend_Test_PHPUnit_ControllerTe return $this->dispatch($url, true); } + + public function postDispatchRaw($url, $data, $headers=[]) { + $this->getRequest() + ->setMethod('POST') + ->setRawBody($data) + ->setHeaders($headers); + + return $this->dispatch($url, true); + } + + /** * Retourne la valeur du header Location * @return String @@ -488,5 +499,3 @@ abstract class AbstractControllerTestCase extends Zend_Test_PHPUnit_ControllerTe var_dump('XHProfile data: ' . BASE_URL."/xhprof/xhprof_html/index.php?run={$run_id}&source=xhprof_testing"); } } - -?> \ No newline at end of file diff --git a/tests/application/modules/admin/controllers/AdminAvisModerationControllerTest.php b/tests/application/modules/admin/controllers/AdminAvisModerationControllerTest.php index 3cbedea6db63b24887a68ca650206547b7342b85..1751f5bce64de6444296ee4ddbd734e23d7502b5 100644 --- a/tests/application/modules/admin/controllers/AdminAvisModerationControllerTest.php +++ b/tests/application/modules/admin/controllers/AdminAvisModerationControllerTest.php @@ -70,7 +70,8 @@ abstract class AdminAvisModerationControllerTestCase extends AbstractControllerT 'date_avis' => '2005-03-27', 'abon_ou_bib' => 0, 'statut' => 0, - 'note' => 4]); + 'note' => 4, + 'source_author' => null]); $this->avis_marcus_routard = $this->fixture('Class_AvisNotice', ['id' => 42, @@ -81,7 +82,8 @@ abstract class AdminAvisModerationControllerTestCase extends AbstractControllerT 'date_avis' => '2010-07-21', 'abon_ou_bib' => 0, 'statut' => 0, - 'note' => 2]); + 'note' => 2, + 'source_author' => null]); } } diff --git a/tests/application/modules/admin/controllers/JournalControllerTest.php b/tests/application/modules/admin/controllers/JournalControllerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..795032bb8541b68b5c5f15beac612837af04de39 --- /dev/null +++ b/tests/application/modules/admin/controllers/JournalControllerTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright (c) 2012, 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 Admin_JournalControllerIndexTest extends Admin_AbstractControllerTestCase { + protected $_storm_default_to_volatile = true; + + public function setUp() { + parent::setUp(); + + Class_Journal::setTimeSource(new TimeSourceForTest('2018-07-06 15:18:04')); + Class_Journal::newInstance(['type' => 'REVIEW_SAVE'])->assertSave(); + + $this->dispatch('/admin/journal', true); + } + + + public function tearDown() { + Class_Journal::setTimeSource(null); + parent::tearDown(); + } + + + /** @test */ + public function titleShouldContainsJournal() { + $this->assertXPathContentContains('//h1', 'Journal d\'évènements'); + } + + + /** @test */ + public function firstEventTypeShouldBePresent() { + $this->assertXPathContentContains('//td', 'REVIEW_SAVE'); + } + + + /** @test */ + public function firstEventCreatedAtShouldBePresent() { + $this->assertXPathContentContains('//td', '2018-07-06 15:18:04', + $this->_response->getBody()); + } +} \ No newline at end of file diff --git a/tests/application/modules/admin/controllers/ModoControllerTest.php b/tests/application/modules/admin/controllers/ModoControllerTest.php index 91df915f932228b6fcfa8af3afa2cfa65ea3dc70..e5fe26e09ec117fba6726d929124ce0e32f1195d 100644 --- a/tests/application/modules/admin/controllers/ModoControllerTest.php +++ b/tests/application/modules/admin/controllers/ModoControllerTest.php @@ -56,7 +56,8 @@ abstract class ModoControllerIndexActionTestCase extends Admin_AbstractControlle 'id_user' => null, 'avis' => 'Ce livre est vraiment bien !', 'statut' => 0, - 'abon_ou_bib' => 1]); + 'abon_ou_bib' => 1, + 'source_author' => null]); $this->fixture('Class_AvisNotice', ['id' => 223, 'id_notice' => 1002, @@ -67,7 +68,8 @@ abstract class ModoControllerIndexActionTestCase extends Admin_AbstractControlle 'avis' => ' Pour faire aimer la biere aux enfants!', 'id_notice' => 1032, 'statut' => 1, - 'abon_ou_bib' => 1]); + 'abon_ou_bib' => 1, + 'source_author' => null]); $this->fixture('Class_Notice', ['id' => 1032, 'titre_principal' => 'B comme bière : la bière expliquée aux (grands) enfants']); @@ -106,7 +108,8 @@ abstract class ModoControllerIndexActionTestCase extends Admin_AbstractControlle 'abon_ou_bib' => 0, 'date_avis' => $i, 'id_notice' => 1032, - 'statut' => 1]); + 'statut' => 1, + 'source_author' => null]); } } @@ -745,7 +748,8 @@ class ModoControllerAvisnoticeActionTest extends Admin_AbstractControllerTestCas 'avis' => 'Un bon livre !', 'id_notice' => 1032, 'statut' => 0, - 'abon_ou_bib' => 1]); + 'abon_ou_bib' => 1, + 'source_author' => null]); $this->dispatch('admin/modo/avisnotice', true); } diff --git a/tests/application/modules/opac/controllers/AbonneControllerAvisTest.php b/tests/application/modules/opac/controllers/AbonneControllerAvisTest.php index 44fe54f783d61da5f369849971f8d7aaa0b92526..07955802c1f1bbfb089e72328d2247eb81e9190e 100644 --- a/tests/application/modules/opac/controllers/AbonneControllerAvisTest.php +++ b/tests/application/modules/opac/controllers/AbonneControllerAvisTest.php @@ -51,7 +51,6 @@ abstract class AbonneFlorenceIsLoggedControllerTestCase extends AbstractControll 'fiche_sigb' => ['type_com' => 0]]); ZendAfi_Auth::getInstance()->logUser($this->florence); - } } @@ -61,9 +60,8 @@ abstract class AbonneControllerAvisTestCase extends AbonneFlorenceIsLoggedContro public function setUp() { parent::setUp(); - $this->potter = Class_Notice::newInstanceWithId(53, - ['clef_oeuvre' =>'POTTER']); - $this->potter->save(); + $this->potter = $this->fixture('Class_Notice', + ['id' => 53, 'clef_oeuvre' => 'POTTER']); } } @@ -104,24 +102,14 @@ class AbonneControllerAvisNoticeWithoutAvisTest extends AbonneControllerAvisTest -class AbonneControllerAvisInvalidNoticeAvisSaveTest extends AbonneControllerAvisTestCase { +class AbonneControllerAvisInvalidNoticeAvisSaveTest extends AbonneControllerAvisTestCase { public $xpath,$json; + public function setUp() { parent::setUp(); - $this->avis_min_saisie = new Class_AdminVar(); - $this->avis_min_saisie - ->setId('AVIS_MIN_SAISIE') - ->setValeur(10); - - $this->avis_max_saisie = new Class_AdminVar(); - $this->avis_max_saisie - ->setId('AVIS_MAX_SAISIE') - ->setValeur(1200); - - Class_AdminVar::getLoader() - ->cacheInstance($this->avis_min_saisie) - ->cacheInstance($this->avis_max_saisie); + Class_AdminVar::set('AVIS_MIN_SAISIE', 10); + Class_AdminVar::set('AVIS_MAX_SAISIE', 1200); } @@ -130,11 +118,10 @@ class AbonneControllerAvisInvalidNoticeAvisSaveTest extends AbonneControllerAvi 'avisTexte' => 'On adore', 'avisNote' => 5, 'avisSignature' => 'FloCouv']; + $this->xpath = new Storm_Test_XPath(); - $this->getRequest() - ->setMethod('POST') - ->setPost($data); - $this->dispatch('/opac/abonne/avis/id_notice/53/render/popup'); + $this->postDispatch('/opac/abonne/avis/id_notice/53/render/popup', $data); + $this->json = json_decode($this->_response->getbody()); $this->assertController('abonne'); $this->assertAction('avis'); @@ -144,68 +131,58 @@ class AbonneControllerAvisInvalidNoticeAvisSaveTest extends AbonneControllerAvi } public function testEmptyEntete() { - $data = array('avisEntete' => '', - 'avisTexte' => 'On adore la magie', - 'avisNote' => 5, - 'avisSignature' => ''); + $data = ['avisEntete' => '', + 'avisTexte' => 'On adore la magie', + 'avisNote' => 5, + 'avisSignature' => '']; + $this->xpath = new Storm_Test_XPath(); - $this->getRequest() - ->setMethod('POST') - ->setPost($data); - $this->dispatch('/opac/abonne/avis/id_notice/53/render/popup'); + $this->postDispatch('/opac/abonne/avis/id_notice/53/render/popup', $data); + $this->json = json_decode($this->_response->getbody()); $this->assertController('abonne'); $this->assertAction('avis'); - $this->xpath->assertXPathContentContains($this->json->content,'//p[@class="error"]','Vous devez saisir un titre',$this->_response->getBody()); + $this->xpath->assertXPathContentContains($this->json->content, + '//p[@class="error"]','Vous devez saisir un titre', + $this->_response->getBody()); } } -class AbonneControllerAvisNoticeAvisSaveTest extends AbonneControllerAvisTestCase { - public function testSaveNewAvis() { - $expected_avis = Class_AvisNotice::newInstance(); - $expected_avis - ->setEntete('Sorcellerie') - ->setAvis('On adore la magie') - ->setNote(5) - ->setClefOeuvre('POTTER') - ->setUser($this->florence) - ->setAbonOuBib(1) - ->setStatut(0); - $expected_avis->save(); - $this->postAndAssertAvisIsSaved($expected_avis); - } - - - public function postAndAssertAvisIsSaved($expected_avis) { +class AbonneControllerAvisNoticeAvisSaveTest extends AbonneControllerAvisTestCase { + /** @test */ + public function newAvisShouldBeSaved() { $data = ['avisEntete' => 'Sorcellerie', 'avisTexte' => 'On adore la magie', 'avisNote' => 5, 'avisSignature' => 'FloCouv']; - $this->getRequest() - ->setMethod('POST') - ->setPost($data); - $this->dispatch('/opac/abonne/avis/id_notice/53'); - - $this->assertEquals('FloCouv', $this->florence->getPseudo()); + $this->postDispatch('/opac/abonne/avis/id_notice/53', $data); + $this->assertNotNull(Class_AvisNotice::findFirstBy(['entete' => 'Sorcellerie'])); } - public function testSaveExistingAvis() { - - $expected_avis = Class_AvisNotice::newInstanceWithId(12); - $expected_avis - ->setEntete('Sorcellerie') - ->setAvis('On adore la magie') - ->setNote(5) - ->setClefOeuvre('POTTER') - ->setUser($this->florence) - ->setAbonOuBib(1) - ->setStatut(0); + /** @test */ + public function existingAvisShouldBeUpdated() { + $this->fixture('Class_AvisNotice', + ['id' => 12, + 'entete' => 'Sorcellerie', + 'avis' => 'On adore la magie', + 'note' => 5, + 'clef_oeuvre' => 'POTTER', + 'user' => $this->florence, + 'abon_ou_bib' => 1, + 'statut' => 0, + 'source_author' => null]); + + $data = ['avisEntete' => 'Sorcellerie mais pas trop', + 'avisTexte' => 'On adore la magie', + 'avisNote' => 4, + 'avisSignature' => 'FloCouv']; - $this->postAndAssertAvisIsSaved($expected_avis); + $this->postDispatch('/opac/abonne/avis/id_notice/53', $data); + $this->assertEquals(4, Class_AvisNotice::find(12)->getNote()); } } @@ -220,7 +197,8 @@ class AbonneControllerAvisNoticeWithAvisTest extends AbonneControllerAvisTestCas 'Entete' => 'Le sorcier super mimi', 'note' => 4, 'clef_oeuvre' => 'POTTER', - 'user' => $this->florence]); + 'user' => $this->florence, + 'source_author' => null]); $this->dispatch('/opac/abonne/avis/id_notice/53/render/popup'); $this->_xpath = new Storm_Test_XPath(); $this->_json = json_decode($this->_response->getBody()); @@ -308,6 +286,7 @@ abstract class AvisControllersFixturesTestCase extends AbonneFlorenceIsLoggedCon 'user' => $this->florence, 'statut' => 0, 'abon_ou_bib'=>1 , + 'source_author' => null, 'notices' => [$this->millenium, $this->millenium_with_vignette] ]); @@ -319,14 +298,15 @@ abstract class AvisControllersFixturesTestCase extends AbonneFlorenceIsLoggedCon $this->avis_potter = $this->fixture('Class_AvisNotice', ['id' => 25, - 'entete' => 'Prenant', - 'avis' => "Mais un peu trop naïf", - 'note'=>4, - 'date_avis' => '2010-10-12 10:00:00', - 'user'=>$this->florence, - 'statut' => 1, - 'abon_out_bib' => 1, - 'notices' => [$this->potter]]); + 'entete' => 'Prenant', + 'avis' => "Mais un peu trop naïf", + 'note'=>4, + 'date_avis' => '2010-10-12 10:00:00', + 'user'=>$this->florence, + 'statut' => 1, + 'abon_out_bib' => 1, + 'source_author' => null, + 'notices' => [$this->potter]]); $lost=$this->fixture('Class_AvisNotice', ['id' => 178, 'entete' => "Lost highway", @@ -338,6 +318,7 @@ abstract class AvisControllersFixturesTestCase extends AbonneFlorenceIsLoggedCon 'statut' => 1, 'flags' => Class_AvisNotice::ORPHAN_FLAG, 'abon_ou_bib'=>1, + 'source_author' => null, 'id_notice' => 30]); @@ -351,6 +332,7 @@ abstract class AvisControllersFixturesTestCase extends AbonneFlorenceIsLoggedCon 'statut' => 1, 'abon_out_bib' => 1, 'flags' => 1, + 'source_author' => null, 'notices' => []]); @@ -372,6 +354,7 @@ abstract class AvisControllersFixturesTestCase extends AbonneFlorenceIsLoggedCon 'user' => $dupont, 'statut' => 0, 'abon_out_bib' => 1, + 'source_author' => null, 'notices' =>[$this->millenium] ]); Storm_Test_ObjectWrapper::onLoaderOfModel('Class_Notice') @@ -845,7 +828,8 @@ class AbonneControllerAvisBlogControllerViewReadAvisTest extends AbonneFlorence ->setUser($this->florence) ->setAbonOuBib(1) ->setStatut(0) - ->setNotices([$millenium]); + ->setNotices([$millenium]) + ->setSourceAuthor(null); } @@ -913,7 +897,8 @@ class AbonneControllerEditAvisNoticeNotAdminLoggedActionTest extends AbstractCon parent::setUp(); $avis = $this->fixture('Class_AvisNotice', ['id' => 54, 'entete' => 'Bonjour !', - 'avis' => 'Ceci est le contenu de l\'avis']); + 'avis' => 'Ceci est le contenu de l\'avis', + 'source_author' => null]); $this->dispatch('/opac/abonne/editavisnotice/id/54', true); } @@ -932,7 +917,8 @@ class AbonneControllerEditAvisNoticeAdminLoggedActionTest extends AbstractContro parent::setUp(); $avis = $this->fixture('Class_AvisNotice', ['id' => 54, 'entete' => 'Bonjour !', - 'avis' => 'Ceci est le contenu de l\'avis']); + 'avis' => 'Ceci est le contenu de l\'avis', + 'source_author' => null]); $this->dispatch('/opac/abonne/editavisnotice/id/54', true); $this->json = json_decode($this->_response->getBody()); @@ -985,6 +971,7 @@ class AbonneControllerEditAvisNoticeAdminLoggedPostActionTest extends AbstractCo 'entete' => 'Bonjour !', 'avis' => 'Ceci est le contenu de l\'avis', 'note' => 1, + 'source_author' => null, 'clef_oeuvre' => 'HUITIEMECOULEURLA-DASTYLE']); $this->fixture('Class_Notice', ['id' => 1190178, @@ -1034,7 +1021,8 @@ class AbonneControllerDeleteAvisNoticeAdminLoggedActionTest extends AbstractCont $_SERVER['HTTP_REFERER'] ='opac/recherche/viewnotice/id/1'; $avis = $this->fixture('Class_AvisNotice', ['id' => 54, 'entete' => 'Bonjour !', - 'avis' => 'Ceci est le contenu de l\'avis']); + 'avis' => 'Ceci est le contenu de l\'avis', + 'source_author' => null]); $this->dispatch('/opac/abonne/delavisnotice/id/54/expressionRecherche/1', true); } diff --git a/tests/application/modules/opac/controllers/BlogControllerTest.php b/tests/application/modules/opac/controllers/BlogControllerTest.php index 6144366da1567f14fad2bcb3d12ee21cdf43f6a8..6516c32892c079d1f87c27c830476a50e3d1a901 100644 --- a/tests/application/modules/opac/controllers/BlogControllerTest.php +++ b/tests/application/modules/opac/controllers/BlogControllerTest.php @@ -76,6 +76,7 @@ class BlogControllerHierarchicalTest extends AbstractControllerTestCase { 'statut' => 1, 'date_avis' => '2015-05-18 00:00:00', 'id_user' => 3, + 'source_author' => null, 'clef_oeuvre' => $this->ksp->getClefOeuvre()]); $this->fixture('Class_AvisNotice', @@ -86,6 +87,7 @@ class BlogControllerHierarchicalTest extends AbstractControllerTestCase { 'statut' => 1, 'date_avis' => '2016-05-18 00:00:00', 'id_user' => 3, + 'source_author' => null, 'clef_oeuvre' => $this->ksp->getClefOeuvre()]); $this->fixture('Class_AvisNotice', @@ -96,6 +98,7 @@ class BlogControllerHierarchicalTest extends AbstractControllerTestCase { 'statut' => 1, 'date_avis' => '2014-05-18 00:00:00', 'id_user' => 3, + 'source_author' => null, 'clef_oeuvre' => $this->ksp->getClefOeuvre()]); $this->fixture('Class_Users', diff --git a/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php b/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php index 49a564d145b07f1986af514bae316a60d905c1a6..7b77a6cccd9e86594844f4ad9821a9d7b38d5a59 100644 --- a/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php +++ b/tests/application/modules/opac/controllers/NoticeAjaxControllerTest.php @@ -1536,6 +1536,8 @@ abstract class NoticeAjaxControllerNoticeWithAvisTestCase 'clef_oeuvre' => 'potter', 'abon_ou_bib' => '0', 'statut' => 1, + 'source_actor_id' => null, + 'source_author' => null, ]); $avis_de_francois = Class_AvisNotice::newInstanceWithId(24, @@ -1545,20 +1547,155 @@ abstract class NoticeAjaxControllerNoticeWithAvisTestCase 'avis'=>'tres bien', 'date_avis'=>'16/02/2013', 'clef_oeuvre'=>'potter', - 'abon_ou_bib'=>'1']) + 'abon_ou_bib' => '1', + 'source_actor_id' => null]) ->setModerationOk(); - Class_Notice::newInstanceWithId(34, - ['titre_principal'=>'Potter', - 'clef_oeuvre'=>'potter', - 'avis' => [$avis_de_paul, - $avis_de_francois]]); + $this->fixture('Class_Notice', ['id' => 34, + 'titre_principal' => 'Potter', + 'clef_oeuvre' => 'potter']); + } +} + + + + +abstract class NoticeAjaxControllerNoticeWithAvisFederationReviewGuestLoggedTestCase + extends NoticeAjaxControllerNoticeWithAvisTestCase { + + protected $_endpoint = 'https://commu.server.io/activitypub/reviews'; + + protected function _loginHook($account) { + $account->ROLE_LEVEL = ZendAfi_Acl_AdminControllerRoles::INVITE; + $account->ROLE = 'invite'; + } + + public function setUp() { + parent::setUp(); + + $this->fixture('Class_Journal', + ['id' => 150, + 'type' => 'AP_REVIEW_DISPLAY_JOIN']); + + $endpoint = $this->_endpoint; + Class_WebService_ActivityPub::setThrowErrors(true); + + $signer = $this->mock() + ->whenCalled('signRequest')->answers('MySignature') + ->whenCalled('getKeyId')->answers($endpoint . '/pubkey') + + ->whenCalled('verify') + ->with(['(request-target)' => 'get /activitypub/reviews/outbox'], 'TheirKey') + ->answers(true); + + Class_WebService_ActivityPub::setSigner($signer); + + $client = $this->mock() + ->whenCalled('open_url') + ->with($endpoint, + ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers(json_encode(['@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Service', + 'id' => $endpoint, + 'inbox' => $endpoint . '/inbox', + 'outbox' => $endpoint . '/outbox', + 'publicKey' => $endpoint . '/pubkey'])) + + ->whenCalled('open_url') + ->with($endpoint . '/pubkey', + ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers(json_encode(['@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Key', + 'id' => $endpoint . '/pubkey', + 'owner' => $endpoint, + 'publicKeyPem' => 'TheirKey'])) + + ->whenCalled('getResponse') + ->answers($this->mock() + ->whenCalled('isError')->answers(false) + ->whenCalled('getHeader')->with('Signature')->answers('TheirSignature') + ->whenCalled('getHeaders')->answers([]) + ->whenCalled('getBody') + ->answers(json_encode(['@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'CollectionPage', + 'totalItems' => 16, + 'items' => [['date_avis' => '2019-04-19 17:46:27', + 'entete' => 'Au top', + 'avis' => 'trop bien !', + 'note' => '4', + 'abon_ou_bib' => 1, + 'source_author' => 'Arcadia']]]))); + + Class_WebService_ActivityPub::setWebClient($client); + } + + + public function tearDown() { + Class_WebService_ActivityPub::setThrowErrors(false); + Class_WebService_ActivityPub::setSigner(null); + Class_WebService_ActivityPub::setWebClient(null); + + parent::tearDown(); } } + +class NoticeAjaxControllerNoticeWithAvisFederationReviewGuestLoggedDisabledTest + extends NoticeAjaxControllerNoticeWithAvisFederationReviewGuestLoggedTestCase { + + public function setUp() { + parent::setUp(); + Class_AdminVar::set('FEDERATION_COMMUNITY_SERVER', ''); + + $this->dispatch('/opac/noticeajax/avis/id_notice/34/page/0/onglet/bloc/cherche/federationreview', true); + } + + + /** @test */ + public function shouldNotHaveCommunityReviews() { + $this->assertNotXPathContentContains('//a', 'Avis communautaires'); + } + + + /** @test */ + public function shouldNotCallWebservice() { + $this->assertFalse(Class_WebService_ActivityPub::getWebClient() + ->methodHasBeenCalled('open_url')); + } +} + + + + +class NoticeAjaxControllerNoticeWithAvisFederationReviewGuestLoggedEnabledTest + extends NoticeAjaxControllerNoticeWithAvisFederationReviewGuestLoggedTestCase { + + public function setUp() { + parent::setUp(); + Class_AdminVar::set('FEDERATION_COMMUNITY_SERVER', $this->_endpoint); + + $this->dispatch('/opac/noticeajax/avis/id_notice/34/page/0/onglet/bloc/cherche/federationreview', true); + } + + + /** @test */ + public function shouldHaveCommunityReviews() { + $this->assertXPathContentContains('//a', 'Avis communautaires'); + } + + + /** @test */ + public function shouldHaveAuthorArcadia() { + $this->assertXPathContentContains('//span[@class="auteur_critique"]', 'Arcadia'); + } +} + + + + class NoticeAjaxControllerNoticeWithAvisEditLinkNotLoggedTest extends NoticeAjaxControllerNoticeWithAvisTestCase { /** @@ -1630,11 +1767,12 @@ class NoticeAjaxControllerNoticeWithAvisWithModerationAndAuthorLostNotLoggedTest -class NoticeAjaxControllerNoticeWithAvisEditLinkGuestLoggedTest extends NoticeAjaxControllerNoticeWithAvisTestCase { +class NoticeAjaxControllerNoticeWithAvisEditLinkGuestLoggedTest + extends NoticeAjaxControllerNoticeWithAvisTestCase { + protected function _loginHook($account) { $account->ROLE_LEVEL = ZendAfi_Acl_AdminControllerRoles::INVITE; $account->ROLE = 'invite'; - } @@ -2495,4 +2633,4 @@ class NoticeAjaxControllerWithKiosqueInResumeTest extends AbstractControllerTest $this->dispatch('/noticeajax/detail/id/2', true); $this->assertXPathContentContains( '//dd', 'Ceci est un logiciel libre.'); } -} \ No newline at end of file +} diff --git a/tests/application/modules/opac/controllers/RssControllerTest.php b/tests/application/modules/opac/controllers/RssControllerTest.php index 7ae5b88f23826320166e4819fd1c9d6ed9b81607..8ebc1fdce4b73767657f87cff2adc104e99fe43a 100644 --- a/tests/application/modules/opac/controllers/RssControllerTest.php +++ b/tests/application/modules/opac/controllers/RssControllerTest.php @@ -366,24 +366,26 @@ class RssControllerCritiquesTest extends AbstractControllerTestCase { ]); $avis = [ - $this->fixture('Class_AvisNotice', [ - 'id' => 1, - 'id_user' => 1, - 'avis' => 'Testing comment 1', - 'entete' => 'Testing comment 1', - 'note' => 4, - 'clef_oeuvre' => 'testing-comment', - 'date_avis' => '2012-01-01', - ]), - $this->fixture('Class_AvisNotice', [ - 'id' => 2, - 'id_user' => 1, - 'avis' => 'Testing comment 2', - 'entete' => 'Testing comment 2', - 'note' => 4, - 'clef_oeuvre' => 'testing-comment', - 'date_avis' => '2012-01-01', - ]), + $this->fixture('Class_AvisNotice', [ + 'id' => 1, + 'id_user' => 1, + 'avis' => 'Testing comment 1', + 'entete' => 'Testing comment 1', + 'note' => 4, + 'clef_oeuvre' => 'testing-comment', + 'source_author' => null, + 'date_avis' => '2012-01-01', + ]), + $this->fixture('Class_AvisNotice', [ + 'id' => 2, + 'id_user' => 1, + 'avis' => 'Testing comment 2', + 'entete' => 'Testing comment 2', + 'note' => 4, + 'clef_oeuvre' => 'testing-comment', + 'date_avis' => '2012-01-01', + 'source_author' => null, + ]), ]; $avis[0]->setNotice($notice1); diff --git a/tests/application/modules/telephone/controllers/BlogControllerTest.php b/tests/application/modules/telephone/controllers/BlogControllerTest.php index 6f95518457fc5a8536a2465daf65a8c7abf78265..298492f889534b3485f0af998a1914e7c332b02a 100644 --- a/tests/application/modules/telephone/controllers/BlogControllerTest.php +++ b/tests/application/modules/telephone/controllers/BlogControllerTest.php @@ -16,7 +16,7 @@ * * 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 + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ require_once 'TelephoneAbstractControllerTestCase.php'; @@ -47,7 +47,8 @@ abstract class Telephone_BlogControllerAvisActionTestCase extends TelephoneAbstr ->setNote(3) ->setEntete('bien') ->setAvis('bla bla') - ->setUser($patouche))); + ->setUser($patouche) + ->setSourceAuthor(null))); } } @@ -119,7 +120,7 @@ class Telephone_BlogControllerViewCritiquesActionTest extends Telephone_BlogCont public function actionShouldBeViewCritiques() { $this->assertAction('viewcritiques'); } - + } ?> \ No newline at end of file diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php index 2173d8d6dfb309de802abc781036f673c1987f1b..e573e0015211d005f209659a65c7c5c4cb9dbd29 100644 --- a/tests/db/UpgradeDBTest.php +++ b/tests/db/UpgradeDBTest.php @@ -2227,7 +2227,6 @@ class UpgradeDB_352_Test extends UpgradeDBTestCase { - class UpgradeDB_353_Test extends UpgradeDBTestCase { public function prepare() {} @@ -2792,4 +2791,131 @@ class UpgradeDB_378_Test extends UpgradeDBTestCase { public function tableExemplaireShouldHaveColumnIdDataProfileInt() { $this->assertFieldType('exemplaires', 'id_data_profile', 'int(11) unsigned'); } -} \ No newline at end of file +} + + + + +class UpgradeDB_379_Test extends UpgradeDBTestCase { + public function prepare() { + $this->dropTable('journal'); + $this->dropTable('journal_detail'); + $this->dropTable('federation_group_membership'); + $this->dropIndexedFieldFrom('notices_avis', 'source_actor_id'); + $this->dropIndexedFieldFrom('notices_avis', 'source_author'); + $this->dropIndexedFieldFrom('notices_avis', 'source_primary'); + } + + + /** @test */ + public function journalTableShouldExists() { + $this->assertTable('journal'); + } + + + public function journalFields() { + return [['id', 'int(11) unsigned'], + ['type', 'varchar(255)'], + ['created_at', 'datetime']]; + } + + + /** + * @test + * @dataProvider journalFields + */ + public function journalFieldShouldExists($field, $type) { + $this->assertFieldType('journal', $field, $type); + } + + + public function journalIndexes() { + return [['type'], ['created_at']]; + } + + + /** + * @test + * @dataProvider journalIndexes + */ + public function journalIndexShouldExists($name) { + $this->assertIndex('journal', $name); + } + + + /** @test */ + public function journalDetailTableShouldExists() { + $this->assertTable('journal_detail'); + } + + + public function journalDetailFields() { + return [['id', 'int(11) unsigned'], + ['journal_id', 'int(11) unsigned'], + ['type', 'varchar(255)'], + ['value', 'text']]; + } + + + /** + * @test + * @dataProvider journalDetailFields + */ + public function journalDetailFieldShouldExists($field, $type) { + $this->assertFieldType('journal_detail', $field, $type); + } + + + public function journalDetailIndexes() { + return [['journal_id'], ['type']]; + } + + + /** + * @test + * @dataProvider journalDetailIndexes + */ + public function journalDetailIndexShouldExists($name) { + $this->assertIndex('journal_detail', $name); + } + + + /** @test */ + public function federationGroupMembershipTableShouldExists() { + $this->assertTable('federation_group_membership'); + } + + + public function federationGroupMembershipFields() { + return [['id', 'int(11) unsigned'], + ['group_name', 'varchar(255)'], + ['actor_id', 'varchar(255)'], + ['accepted_at', 'datetime']]; + } + + + /** + * @test + * @dataProvider federationGroupMembershipFields + */ + public function federationGroupMembershipFieldShouldExists($field, $type) { + $this->assertFieldType('federation_group_membership', $field, $type); + } + + + public function noticesAvisNewFields() { + return [['source_actor_id', 'varchar(255)'], + ['source_author', 'varchar(255)'], + ['source_primary', 'int(11)']]; + } + + + /** + * @test + * @dataProvider noticesAvisNewFields + */ + public function noticesAvisNewFieldShouldExistsAndBeIndexed($field, $type) { + $this->assertFieldType('notices_avis', $field, $type); + $this->assertIndex('notices_avis', $field); + } +} diff --git a/tests/library/Class/HttpSignatureTest.php b/tests/library/Class/HttpSignatureTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9ea89ca215e9c512c008d82a1fc8c96e4c916770 --- /dev/null +++ b/tests/library/Class/HttpSignatureTest.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright (c) 2012-2019, 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_HttpSignatureTest extends ModelTestCase { + protected + $_pubkey = '-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmUEi4rcQARpDBIUoQI7E +D9/WidC0ILPrxhWPcHg6+Mo24Mkj9cKER2E1jZ+nMv0xfKeAt6juXqOxWqD2CUuh +Dgp3ndJkI+9x8sLUHnGBIprUa8c++CVJ6nsMqzHoCMzRTYvbeaFkYjRGWDQES/0J +Co/BtrM0csuRkRunJb98SqkGaP0+mhmDljphebqHvtAAsU1N3jc1BY2/HLuzPADd +2fOEcvsYPXd5YGp/DnfhejyctC4w+NpGZobaZ8jtp4AacXVcox9SJ1C07zqZzxhP +r1ieSwML9mDKueYe6BjdYFhXEwV2+7fsqNykV3dZDs/5reGyFLMqIMcE4VDMojY8 +yQIDAQAB +-----END PUBLIC KEY-----', + + $_privkey = '-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAmUEi4rcQARpDBIUoQI7ED9/WidC0ILPrxhWPcHg6+Mo24Mkj +9cKER2E1jZ+nMv0xfKeAt6juXqOxWqD2CUuhDgp3ndJkI+9x8sLUHnGBIprUa8c+ ++CVJ6nsMqzHoCMzRTYvbeaFkYjRGWDQES/0JCo/BtrM0csuRkRunJb98SqkGaP0+ +mhmDljphebqHvtAAsU1N3jc1BY2/HLuzPADd2fOEcvsYPXd5YGp/DnfhejyctC4w ++NpGZobaZ8jtp4AacXVcox9SJ1C07zqZzxhPr1ieSwML9mDKueYe6BjdYFhXEwV2 ++7fsqNykV3dZDs/5reGyFLMqIMcE4VDMojY8yQIDAQABAoIBABR3TmFYcRq0lx6T +aby1VBmKmuvsoyF65ZGeb3lllPqEhq+eLN81CtU9dhljqMB2b5VmCRp9xNd+pMCl +njW/k9J8M10wK49g+qagvhMStVwZsSRzh0U8NZLKu/Zgw8vpDkp80uJ7WxyCPqKo +z6oWMI7og8YSSH7MELSALOItoDuYAgfto0oA/b1KYcXxJ0lYmC08D0nBM1Dug2/W +UVeiA3pw1J9KZAgjj4lK4ZiN7JqY+FbIogbgR0jKePLTjECETY3X2LZ1Yvth8TP9 +vdgHLUkGQfh/ZMbK44oBEX/2qiacVi1XAzuhlrz3+NyvgCtqMgkMS/uEMN+HgvCR +w9vIIwECgYEAyucnH7vio1MsVkL9uu5cZEQDWDxV3fPWNMrPZ+sPccqmCI30fUWF +0w5vJHyt0AaviW1KYAAhkpnj+qtsKiU8Xx6PeOQoJ04plEDGPENtkRlGaEAzABlX +PAdMo2YjGPI7wuJbTMlzlsY8BhuKKYrgaXTdZNDnTjtu2Xgr9/3JDU0CgYEAwVvw +ADlZwAUKU77XjrrtP5dJIA0KJHZI9npatZ0hTUZmyKTwCzde8FuBWngQHk5qW1t0 +LD9SuwWuhfUX/lvfKqWTy+1ak6jN1pAQoE1Gb3yx8q+C56VasEBFX2WkCcZgEN1x +DRpB7RgEmeLCsF25WV75Y/f5XH0zl8CGC/BcX20CgYEAso5SvrlwC7yg8tSHRx6G +DfJQYzDNe8IeCl1DwjZ4Y/IqxLJvqmIpD3/PTPOvXbbUeQLFhc/3u3RTzP9X84rL +IwXYylE2CMjfDEkoalYIML1mWU3N09N5Eil2RwEV99kLwEfEgsFxSAjxP4qyvjYp +oIQoZJT2SMFCnnwDbXxXlq0CgYEAmEBgVozSEtTlMNQQv56IuY3SUp5x4gwRn6Lw +UhkL4+EPheX57ZsH8pLa4/WuG277aDw22bBy4Di1F13KKssEinweSHD45VQB4HVH +4jF2yMqTA9kXZndZVXcGKPvLkrbVZfI31m1ag+pplRJs4pqqG6khDopvm1gqi89Y +vYXh9nECgYEAm9f+N3Mxx/bGRzU5D1IjIwwPCynEx8M/NLmdu3GWstRjl8B9lTKG +OSm8iswUChadsHDhP1slvjeywDE7pTh8IPunmwFuc7/Q0J0vAmNHl8+oeAqwVJ82 +wQY7fGvKXZZ06i7+GfBDH0wUvUESJYR2strZqfaSB+GelLV9vp0p7iE= +-----END RSA PRIVATE KEY-----'; + + + /** @test */ + public function signingRsaShouldReturn344CharsLongSignature() { + $sign = (new Class_HttpSignature('')) + ->sign(['(request-target)' => 'post /activitypub/reviews', + 'date' => 'Thu, 21 Dec 2000 16:01:07 +0100', + 'digest' => '99914b932bd37a50b983c5e7c90ae93b'], + Class_HttpSignature::REQUEST_TARGET . ' date digest', + $this->_privkey, + 'rsa-sha256'); + + $this->assertEquals(344, strlen($sign)); + } + + + /** @test */ + public function verifyRsaShouldSucceedWithKnownValidSignature() { + $sign = new Class_HttpSignature('keyId="rsa-key-1",algorithm="rsa-sha256",headers="(request-target) date digest",signature="dfflJw4lqkpVh9aOwXPLdCqSM8LmD2IX0jDUgVNFZ/frkjGttriewAq2iUAYFWNr9oWvHt6QaW/sspydDyYl752PlSAFFjDewgk6m+lhhMasMwWrST4U+16AXuFpjJJoPWlWuTHgpfcYz2QquPTIcA4TfA5ANORzISfsMIRyb7eJBD9W/d3qSXutn5RuBLLrp0Hx2abGM31rr4gzzZUJ6OhocTI2ZA0XdjuXGF/eIexqV4snlnXUXEpZULW+B6iwPHYj23ADRz3SwF9GpovrEOhYXxmLgAYHY9ErV/82YWr9+NIXPwc/UL4zpZSHqMab0T3+IC/qK+U2g2XW9QKqZA=="'); + + $this->assertTrue($sign->verify(['(request-target)' => 'post /activitypub/reviews', + 'date' => 'Thu, 21 Dec 2000 16:01:07 +0100', + 'digest' => '99914b932bd37a50b983c5e7c90ae93b'], + $this->_pubkey)); + } + + + /** @test */ + public function verifyRsaShouldFailWithBadSignature() { + $sign = new Class_HttpSignature('keyId="rsa-key-1",algorithm="rsa-sha256",headers="(request-target) date digest",signature="pHxV6i6cn9ySijY46m+3Tx+NZpCVwEQ0YWNowa4KWAJmI9wNd9hDwlSOG++Hf/vuAM9Pi2bGgaCT7y1I3jNB1fcsi9QlBVK1vW7o7RM5qL55fSHYOvx03KWb0n4ed4D+rcTiO9Pv74+ERMZAgTX4pEzFqJlr1v7UO6Qe5cM6WRT461L8KASwtRJgJ1vmRNLisNZz+YWuQNz9C0Da2y/YWEryxPeZxSZCi0T3bDI/fUNmzLpxNwVtijwH2xivRmcGoUFvB/YEgOSLdJhJf/2AvbXQi4A+C2+OqFPeHx5hkLhoSLLjMwh93wDfIjrfdy7eN4jjVRdn5UYggiWDKxhR+g=="'); + + $this->assertFalse($sign->verify(['(request-target)' => 'post /activitypub/reviews', + 'date' => 'Thu, 21 Dec 2000 16:01:07 +0100', + 'digest' => '99914b932bd37a50b983c5e7c90ae93b'], + $this->_pubkey)); + } + + + /** @test */ + public function verifyRsaShouldFailWithBadPubkey() { + $sign = new Class_HttpSignature('keyId="rsa-key-1",algorithm="rsa-sha256",headers="(request-target) date digest",signature="dfflJw4lqkpVh9aOwXPLdCqSM8LmD2IX0jDUgVNFZ/frkjGttriewAq2iUAYFWNr9oWvHt6QaW/sspydDyYl752PlSAFFjDewgk6m+lhhMasMwWrST4U+16AXuFpjJJoPWlWuTHgpfcYz2QquPTIcA4TfA5ANORzISfsMIRyb7eJBD9W/d3qSXutn5RuBLLrp0Hx2abGM31rr4gzzZUJ6OhocTI2ZA0XdjuXGF/eIexqV4snlnXUXEpZULW+B6iwPHYj23ADRz3SwF9GpovrEOhYXxmLgAYHY9ErV/82YWr9+NIXPwc/UL4zpZSHqMab0T3+IC/qK+U2g2XW9QKqZA=="'); + + $bad_key = '-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9AYHqfFq69j/nCKxOI5T +NaJgXVYVTXqUZCrwMTrTViUf4CdhenW/akMKhPR7+Z6okFAwnY6DWRZUanDp9jYV +WLS4TtMq4svS8E0i94vWWg3x+6vBuuScZaYksM6nOjaOVvd1SIqvZzrVO2QrYfd9 +O/gV5k6ySPhcWYuPfcT+EFUkcwKQ/4enXiSZO9lKi6uKgaabVHS28JgYv1991cau +eZgVoRGTvjeHg0oZmIygKeV03xqqB87Hry4HlBAgWTL9TsEjdMNiVpVNxtXdHYi7 +eRzZBqZNEl5LphJoTKhb+A/wPIskqHXGUvvCqTgY7ofNtOTsZdQALthWrBNvHMga +nQIDAQAB +-----END PUBLIC KEY-----'; + + $this->assertFalse($sign->verify(['(request-target)' => 'post /activitypub/reviews', + 'date' => 'Thu, 21 Dec 2000 16:01:07 +0100', + 'digest' => '99914b932bd37a50b983c5e7c90ae93b'], + $bad_key)); + } +} diff --git a/tests/library/Class/NoticeTest.php b/tests/library/Class/NoticeTest.php index 4c21255d917c6d6babbd2a543d104d698fe139de..41f1089c824584d5d2fbc8219e73bd57b0821b24 100644 --- a/tests/library/Class/NoticeTest.php +++ b/tests/library/Class/NoticeTest.php @@ -446,8 +446,11 @@ class NoticeTestTypeDoc extends ModelTestCase { class NoticeTestGetAvis extends ModelTestCase { public function setUp() { parent::setUp(); + $base_properties = ['avis' => 'Testing comment', - 'entete' => 'Testing comment']; + 'entete' => 'Testing comment', + 'clef_oeuvre' => 'TESTING-RECORD--', + 'source_actor_id' => null]; $user_bib = $this->fixture('Class_Users', ['id' => 1, @@ -478,12 +481,6 @@ class NoticeTestGetAvis extends ModelTestCase { ['id' => 4, 'abon_ou_bib' => 0, 'note' => 3])); - $this->onLoaderOfModel('Class_AvisNotice') - ->whenCalled('findAllBy') - ->with(['clef_oeuvre' => 'TESTING-RECORD--']) - ->answers([$this->avis_bib1, $this->avis_bib2, - $this->avis_abon1, $this->avis_abon2]) - ->beStrict(); $this->notice = $this->fixture('Class_Notice', ['id' => 12, diff --git a/tests/library/ZendAfi/View/Helper/Abonne/AbonnementTest.php b/tests/library/ZendAfi/View/Helper/Abonne/AbonnementTest.php index 023c743fa742119df5f74c611dc3f3a6ba1d5a3a..c402e75a86b0ab243add2348ab1595ce6519efb2 100644 --- a/tests/library/ZendAfi/View/Helper/Abonne/AbonnementTest.php +++ b/tests/library/ZendAfi/View/Helper/Abonne/AbonnementTest.php @@ -18,6 +18,7 @@ * along with BOKEH; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + require_once 'library/ZendAfi/View/Helper/ViewHelperTestCase.php'; class View_Helper_Abonne_AbonnementTest extends ViewHelperTestCase { @@ -28,8 +29,17 @@ class View_Helper_Abonne_AbonnementTest extends ViewHelperTestCase { public function setUp() { parent::setUp(); + + Class_User_ILSSubscription::setTimeSource(new TimeSourceForTest('2018-10-03')); + $this->_helper = new ZendAfi_View_Helper_Abonne_Abonnement(); - $this->_helper->setView(new ZendAfi_Controller_Action_Helper_View()); + $this->_helper->setView($this->view); + } + + + public function tearDown() { + Class_User_ILSSubscription::setTimeSource(null); + parent::tearDown(); } @@ -48,7 +58,4 @@ class View_Helper_Abonne_AbonnementTest extends ViewHelperTestCase { '//div[@class="abonnement"]', 'Votre abonnement est valide'); } - -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/tests/library/ZendAfi/View/Helper/Accueil/CritiquesTest.php b/tests/library/ZendAfi/View/Helper/Accueil/CritiquesTest.php index 431f986365bd281025e58d9e79711337667f1702..d88f4113ceff7729c4f7b52949c8bceb03b7f97a 100644 --- a/tests/library/ZendAfi/View/Helper/Accueil/CritiquesTest.php +++ b/tests/library/ZendAfi/View/Helper/Accueil/CritiquesTest.php @@ -95,6 +95,7 @@ abstract class CritiquesAvisTestCase extends ViewHelperTestCase { 'note' => 5, 'date_avis' => '2010-03-18 13:00:00', 'user' => $lolo, + 'source_author' => null, 'statut' => 1, 'notices' => [$millenium]]); @@ -106,6 +107,7 @@ abstract class CritiquesAvisTestCase extends ViewHelperTestCase { 'date_avis' => '2010-03-18 13:00:00', 'user' => $super_lolo, 'statut' => 1, + 'source_author' => null, 'notices' => [$millenium]]); $avis_millenium_from_suplo_with_html = $this->fixture('Class_AvisNotice', @@ -116,6 +118,7 @@ abstract class CritiquesAvisTestCase extends ViewHelperTestCase { 'date_avis' => '2010-03-18 13:00:00', 'user' => $super_lolo, 'statut' => 1, + 'source_author' => null, 'notices' => [$millenium]]); $avis_orphan = $this->fixture('Class_AvisNotice', @@ -126,6 +129,7 @@ abstract class CritiquesAvisTestCase extends ViewHelperTestCase { 'date_avis' => '2010-03-18 13:00:00', 'user' => $lolo, 'abon_ou_bib' => 0, + 'source_author' => null, 'statut' => 1, 'notices' => []]); @@ -142,6 +146,7 @@ abstract class CritiquesAvisTestCase extends ViewHelperTestCase { 'date_avis' => '2010-03-18 13:00:00', 'user' => $lolo, 'abon_ou_bib' => 0, + 'source_author' => null, 'statut' => 1, 'notices' => [$potter]]); diff --git a/tests/library/ZendAfi/View/Helper/AvisTest.php b/tests/library/ZendAfi/View/Helper/AvisTest.php index be52f9d785600e08bdd51c433be0152258407dc8..fec5ed23b50b81a1755cde60d32797e6bee8e5a7 100644 --- a/tests/library/ZendAfi/View/Helper/AvisTest.php +++ b/tests/library/ZendAfi/View/Helper/AvisTest.php @@ -57,6 +57,7 @@ class ViewHelperAvisTestWithAvisNotice extends ViewHelperTestCase { ->setUser($lolo) ->setAbonOuBib(0) ->setStatut(1) + ->setSourceAuthor(null) ->setNotices(array($millenium, $millenium_with_vignette)); @@ -199,6 +200,7 @@ class ViewHelperAvisTestAsBib extends ViewHelperTestCase { ->setUser($tintin) ->setStatut(0) ->setAbonOuBib(1) + ->setSourceAuthor(null) ->setNotices(array()); $helper = new ZendAfi_View_Helper_Avis(); @@ -209,9 +211,10 @@ class ViewHelperAvisTestAsBib extends ViewHelperTestCase { public function testBrInAvis() { $this->assertTrue(strpos($this->html, "Pas<br />\nterrible") !== false, $this->html); } +} + -} class ViewHelperAvisTestWithoutAvisNoticeAndModeration extends ViewHelperTestCase { public function setUp() { @@ -234,6 +237,7 @@ class ViewHelperAvisTestWithoutAvisNoticeAndModeration extends ViewHelperTestCas ->setUser($tintin) ->setStatut(0) ->setAbonOuBib(0) + ->setSourceAuthor(null) ->setNotices(array()); @@ -339,6 +343,7 @@ class ViewHelperAvisTestHtmlForCritiquesModule extends ViewHelperTestCase { 'user' => $tintin, 'abon_ou_bib' => 0, 'statut' => 0, + 'source_author' => null, 'notices' => [$millenium]]); $helper = new ZendAfi_View_Helper_Avis(); @@ -384,6 +389,7 @@ class ViewHelperAvisAmazonTestContenuAvisHtml extends ViewHelperTestCase { ->setDateAvis('2010-03-18 13:00:00') ->setAbonOuBib(0) ->setStatut(0) + ->setSourceAuthor(null) ->setUser(null); $helper = new ZendAfi_View_Helper_Avis(); diff --git a/tests/library/ZendAfi/View/Helper/Notice/AvisTest.php b/tests/library/ZendAfi/View/Helper/Notice/AvisTest.php index c6db98c423c3653c2967745199266430c8778de9..ca030a2c7c3851a2f34b2aac07b7925de7009316 100644 --- a/tests/library/ZendAfi/View/Helper/Notice/AvisTest.php +++ b/tests/library/ZendAfi/View/Helper/Notice/AvisTest.php @@ -21,80 +21,62 @@ class ZendAfi_View_Helper_Notice_AvisTest extends ViewHelperTestCase { - protected $_html; + protected + $_storm_default_to_volatile = true, + $_millenium, + $_avis; public function setUp() { parent::setUp(); - $this->_helper = new ZendAfi_View_Helper_Notice_Avis(); - $this->_helper->setView(new ZendAfi_Controller_Action_Helper_View()); - - $this->_view = new ZendAfi_Controller_Action_Helper_View(); - - Class_Profil::setCurrentProfil(new Class_Profil()); - Zend_Registry::get('locale')->setLocale('fr'); - Zend_Registry::get('translate')->setLocale('fr'); - $this->millenium = new Class_Notice(); - $this->millenium - ->setId(25); + $this->_helper = new ZendAfi_View_Helper_Notice_Avis(); + $this->_helper->setView($this->view); - $this->avis = array("bib" => array("nombre" => 0), - "abonne" => array("nombre" => 0)); + $this->_millenium = $this->fixture('Class_Notice', ['id' => 25]); - $this->avis_bib_seulement = Class_AdminVar::newInstanceWithId('AVIS_BIB_SEULEMENT'); + $this->_avis = ["bib" => new Class_Notice_ReviewsSet('Bibliothécaires', [], 0, 0), + "abonne" => new Class_Notice_ReviewsSet('Lecteurs du portail', [], 0, 0)]; - $account = new stdClass(); - $account->username = 'AutoTest' . time(); - $account->password = md5( 'password' ); - $account->ID_USER = 0; - $account->ROLE_LEVEL = 4; - $account->confirmed = true; - $account->enabled = true; - ZendAfi_Auth::getInstance()->getStorage()->write($account); + $this->login(ZendAfi_Acl_AdminControllerRoles::ADMIN_BIB); } - public function testVisibleWithAdminAndAvisReaderAllowed() { - ZendAfi_Auth::getInstance()->getIdentity()->ROLE_LEVEL = 5; - $this->avis_bib_seulement->setValeur('0'); + /** @test */ + public function modoPortailWithAvisReaderAllowedShouldBeAbleToAddReview() { + Class_Users::getIdentity()->beModoPortail(); + Class_AdminVar::set('AVIS_BIB_SEULEMENT', '0'); - $html = $this->_helper->Notice_Avis($this->millenium, $this->avis, ['onglet' => 'bloc']); - $this->assertTrue(strpos($html, 'Donnez ou modifiez votre avis') != false, $html); + $html = $this->_helper->notice_Avis($this->_millenium, $this->_avis, ['onglet' => 'bloc']); + $this->assertContains('Donnez ou modifiez votre avis', $html); } - public function testVisibleWithReaderAndAvisReaderAllowed() { - ZendAfi_Auth::getInstance()->getIdentity()->ROLE_LEVEL = 1; - $this->avis_bib_seulement->setValeur('0'); + /** @test */ + public function inviteWithAvisReaderAllowedShouldBeAbleToAddReview() { + Class_Users::getIdentity()->beInvite(); + Class_AdminVar::set('AVIS_BIB_SEULEMENT', '0'); - $html = $this->_helper->Notice_Avis($this->millenium, $this->avis, ['onglet' => 'bloc']); - $this->assertTrue(strpos($html, 'Donnez ou modifiez votre avis') != false); + $html = $this->_helper->notice_Avis($this->_millenium, $this->_avis, ['onglet' => 'bloc']); + $this->assertContains('Donnez ou modifiez votre avis', $html); } - public function testVisibleWithAdminAndAvisReaderForbidden() { - ZendAfi_Auth::getInstance()->getIdentity()->ROLE_LEVEL = 5; - $this->avis_bib_seulement->setValeur('1'); + /** @test */ + public function modoPortailAndAvisReaderForbiddenShouldBeAbleToAddReview() { + Class_Users::getIdentity()->beModoPortail(); + Class_AdminVar::set('AVIS_BIB_SEULEMENT', '1'); - $html = $this->_helper->Notice_Avis($this->millenium, $this->avis, ['onglet' => 'bloc']); - $this->assertTrue(strpos($html, 'Donnez ou modifiez votre avis') != false); + $html = $this->_helper->Notice_Avis($this->_millenium, $this->_avis, ['onglet' => 'bloc']); + $this->assertContains('Donnez ou modifiez votre avis', $html); } + /** @test */ public function testInvisibleWithReaderAndAvisReaderForbidden() { - $user = $this->fixture('Class_Users', - ['id' => 1, - 'login' => 'pom', - 'password' => '123', - ]); - $user->setRoleLevel(ZendAfi_Acl_AdminControllerRoles::INVITE)->save(); - - ZendAfi_Auth::getInstance()->logUser($user); - - $this->avis_bib_seulement->setValeur('1'); + Class_Users::getIdentity()->beInvite(); + Class_AdminVar::set('AVIS_BIB_SEULEMENT', '1'); - $html = $this->_helper->Notice_Avis($this->millenium, $this->avis, ['onglet' => 'bloc']); - $this->assertFalse(strpos($html, 'Donnez ou modifiez votre avis')); + $html = $this->_helper->Notice_Avis($this->_millenium, $this->_avis, ['onglet' => 'bloc']); + $this->assertNotContains('Donnez ou modifiez votre avis', $html); } } -?> \ No newline at end of file diff --git a/tests/scenarios/Activitypub/ActivitypubAdminTest.php b/tests/scenarios/Activitypub/ActivitypubAdminTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0b267c6967f56512926623e13437a717da8cc672 --- /dev/null +++ b/tests/scenarios/Activitypub/ActivitypubAdminTest.php @@ -0,0 +1,383 @@ +<?php +/** + * Copyright (c) 2012-2017, 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 __DIR__ . '/../../../library/activitystreams/autoload.php'; + +use Patbator\ActivityStreams\Model\Service; +use Patbator\ActivityStreams\Stream; + + + +class ActivitypubAdminMenuDisabledTest extends Admin_AbstractControllerTestCase { + protected $_storm_default_to_volatile = true; + + public function setUp() { + parent::setUp(); + Class_AdminVar::set('FEDERATION_COMMUNITY_SERVER', ''); + + $this->dispatch('/admin', true); + } + + + /** @test */ + public function federationMenuShouldNotBePresent() { + $this->assertNotXPathContentContains('//td', 'Fédération'); + } + + + /** @test */ + public function reviewsLinkShouldNotBePresent() { + $this->assertNotXPathContentContains('//a[contains(@href, "/federation-reviews")]', 'Avis'); + } +} + + + + +abstract class ActivitypubAdminEnabledTestCase extends Admin_AbstractControllerTestCase { + protected $_storm_default_to_volatile = true; + + public function setUp() { + parent::setUp(); + Class_AdminVar::set('FEDERATION_COMMUNITY_SERVER', + 'https://reviews.my-server.io/activitypub/reviews'); + } +} + + + + +class ActivitypubAdminMenuEnabledTest extends ActivitypubAdminEnabledTestCase { + public function setUp() { + parent::setUp(); + $this->dispatch('/admin', true); + } + + + /** @test */ + public function federationMenuShouldBePresent() { + $this->assertXPathContentContains('//td', 'Fédération'); + } + + + /** @test */ + public function reviewsLinkShouldBePresent() { + $this->assertXPathContentContains('//a[contains(@href, "/federation-reviews")]', 'Avis'); + } + + + /** @test */ + public function journalMenuShouldBePresent() { + $this->assertXPathContentContains('//td', 'Journal'); + } +} + + + + +class ActivitypubAdminFederationReviewsControllerIndexTest extends ActivitypubAdminEnabledTestCase { + public function setUp() { + parent::setUp(); + $this->dispatch('/admin/federation-reviews'); + } + + + /** @test */ + public function pageShouldContainsEnableDisplay() { + $this->assertXPath('//button[contains(@data-url, "/federation-reviews/enable-display")]'); + } + + + /** @test */ + public function pageShouldContainsEnableShare() { + $this->assertXPath('//button[contains(@data-url, "/federation-reviews/enable-share")]'); + } +} + + + + +class ActivitypubAdminFederationReviewsControllerEnableDisplayWithoutCommunityServerTest + extends Admin_AbstractControllerTestCase { + protected $_storm_default_to_volatile = true; + + + public function setUp() { + parent::setUp(); + $this->dispatch('/admin/federation-reviews/enable-display'); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirect(); + } + + + /** @test */ + public function reviewDisplayShouldNotBeEnabled() { + $this->assertFalse(Class_FederationReview::getInstance()->isDisplayEnabled()); + } +} + + + +abstract class ActivitypubAdminFederationReviewsControllerWithCommunityServerTestCase + extends ActivitypubAdminEnabledTestCase { + + protected $_signer; + + + public function setUp() { + parent::setUp(); + + $endpoint = Class_AdminVar::get('FEDERATION_COMMUNITY_SERVER'); + Class_WebService_ActivityPub::setThrowErrors(true); + + $this->_signer = $this + ->mock() + ->whenCalled('signRequest')->answers('MySignature') + ->whenCalled('getKeyId')->answers($endpoint . '/pubkey') + + ->whenCalled('verify') + ->with(['(request-target)' => 'post /activitypub/reviews/inbox'], 'TheirKey') + ->answers(true); + + Class_WebService_ActivityPub::setSigner($this->_signer); + + $client = $this->mock() + ->whenCalled('open_url') + ->with($endpoint, + ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers(json_encode(['@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Service', + 'id' => $endpoint, + 'inbox' => $endpoint . '/inbox', + 'publicKey' => $endpoint . '/pubkey'])) + + ->whenCalled('open_url') + ->with($endpoint . '/pubkey', + ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers(json_encode(['@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Key', + 'id' => $endpoint . '/pubkey', + 'owner' => $endpoint, + 'publicKeyPem' => 'TheirKey'])) + + ->whenCalled('postRawDataResponse') + ->answers($this->mock() + ->whenCalled('isError')->answers(false) + ->whenCalled('getHeader')->with('Signature')->answers('TheirSignature') + ->whenCalled('getHeaders')->answers([]) + ->whenCalled('getBody') + ->answers(json_encode(['@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Accept']))) + ; + + Class_WebService_ActivityPub::setWebClient($client); + } + + + public function tearDown() { + Class_WebService_ActivityPub::setThrowErrors(false); + Class_WebService_ActivityPub::setSigner(null); + parent::tearDown(); + } +} + + + +class ActivitypubAdminFederationReviewsControllerEnableDisplayTest + extends ActivitypubAdminFederationReviewsControllerWithCommunityServerTestCase { + + protected $_json; + + public function setUp() { + parent::setUp(); + Class_AdminVar::set('FEDERATION_ACTOR_NAME', 'Arcadia'); + + $this->dispatch('/admin/federation-reviews/enable-display'); + $params = Class_WebService_ActivityPub::getWebClient() + ->getAttributesForLastCallOn('postRawDataResponse'); + + $this->_json = json_decode($params[1], true); + } + + + /** @test */ + public function sentNameShouldBeArcadia() { + $this->assertEquals('Arcadia', $this->_json['actor']['name']); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirectTo('/admin/federation-reviews/index'); + } + + + /** @test */ + public function reviewDisplayShouldBeEnabled() { + $this->assertTrue(Class_FederationReview::getInstance()->isDisplayEnabled()); + } + + + /** @test */ + public function federationPubkeyShouldNotBeEmpty() { + $pubkey = Class_AdminVar::get('FEDERATION_PUBKEY'); + $this->assertContains('-----BEGIN PUBLIC KEY-----', $pubkey); + $this->assertContains('-----END PUBLIC KEY-----', $pubkey); + } + + + /** @test */ + public function federationPrivkeyShouldNotBeEmpty() { + $privkey = Class_AdminVar::get('FEDERATION_PRIVKEY'); + $this->assertContains('-----BEGIN RSA PRIVATE KEY-----', $privkey); + $this->assertContains('-----END RSA PRIVATE KEY-----', $privkey); + } +} + + + +class ActivitypubAdminFederationReviewsControllerEnableShareWithoutCommunityServerTest + extends Admin_AbstractControllerTestCase { + protected $_storm_default_to_volatile = true; + + + public function setUp() { + parent::setUp(); + $this->dispatch('/admin/federation-reviews/enable-share'); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirect(); + } + + + /** @test */ + public function reviewShareShouldNotBeEnabled() { + $this->assertFalse(Class_FederationReview::getInstance()->isShareEnabled()); + } +} + + + +class ActivitypubAdminFederationReviewsControllerEnableShareTest + extends ActivitypubAdminFederationReviewsControllerWithCommunityServerTestCase { + + public function setUp() { + parent::setUp(); + $this->dispatch('/admin/federation-reviews/enable-share'); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirectTo('/admin/federation-reviews/index'); + } + + + /** @test */ + public function reviewShareShouldBeEnabled() { + $this->assertTrue(Class_FederationReview::getInstance()->isShareEnabled(), + json_encode($this->_getFlashMessengerNotifications(), JSON_PRETTY_PRINT)); + } +} + + + +class ActivitypubAdminFederationReviewsControllerIndexAllEnabledTest + extends ActivitypubAdminEnabledTestCase { + protected $_storm_default_to_volatile = true; + + + public function setUp() { + parent::setUp(); + $this->fixture('Class_Journal', + ['id' => 150, + 'type' => 'AP_REVIEW_DISPLAY_JOIN']); + $this->fixture('Class_Journal', + ['id' => 151, + 'type' => 'AP_REVIEW_SHARE_JOIN']); + + $this->dispatch('/admin/federation-reviews'); + } + + + /** @test */ + public function pageShouldContainsDisableDisplay() { + $this->assertXPath('//button[contains(@data-url, "/federation-reviews/disable-display")]'); + } + + + /** @test */ + public function pageShouldContainsDisableShare() { + $this->assertXPath('//button[contains(@data-url, "/federation-reviews/disable-share")]'); + } +} + + + +class ActivitypubAdminFederationReviewsControllerDisableDisplayTest + extends ActivitypubAdminFederationReviewsControllerWithCommunityServerTestCase { + + public function setUp() { + parent::setUp(); + $this->dispatch('/admin/federation-reviews/disable-display'); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirectTo('/admin/federation-reviews/index'); + } + + + /** @test */ + public function reviewDisplayShouldBeDisabled() { + $this->assertFalse(Class_FederationReview::getInstance()->isDisplayEnabled()); + } +} + + + +class ActivitypubAdminFederationReviewsControllerDisableShareTest + extends ActivitypubAdminFederationReviewsControllerWithCommunityServerTestCase { + + public function setUp() { + parent::setUp(); + $this->dispatch('/admin/federation-reviews/disable-share'); + } + + + /** @test */ + public function shouldRedirect() { + $this->assertRedirectTo('/admin/federation-reviews/index'); + } + + + /** @test */ + public function reviewShareShouldBeDisabled() { + $this->assertFalse(Class_FederationReview::getInstance()->isShareEnabled()); + } +} diff --git a/tests/scenarios/Activitypub/ActivitypubReviewTest.php b/tests/scenarios/Activitypub/ActivitypubReviewTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4ae91337733672b8c5ad0b43194e4e038a55690a --- /dev/null +++ b/tests/scenarios/Activitypub/ActivitypubReviewTest.php @@ -0,0 +1,883 @@ +<?php +/** + * Copyright (c) 2012-2017, 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 __DIR__ . '/../../../library/activitystreams/autoload.php'; +require_once __DIR__ . '/../../../application/modules/activitypub/controllers/ReviewController.php'; + +use Patbator\ActivityStreams\Model\Follow; +use Patbator\ActivityStreams\Model\Accept; +use Patbator\ActivityStreams\Model\Reject; +use Patbator\ActivityStreams\Model\Service; +use Patbator\ActivityStreams\Model\Join; +use Patbator\ActivityStreams\Model\Leave; +use Patbator\ActivityStreams\Model\Group; +use Patbator\ActivityStreams\Stream; + + +abstract class ActivitypubReviewTestCase extends AbstractControllerTestCase { + protected + $_storm_default_to_volatile = true, + $_json; +} + + + +abstract class ActivitypubReviewClientTestCase extends ActivitypubReviewTestCase { + public function setUp() { + parent::setUp(); + Class_AdminVar::set('FEDERATION_COMMUNITY_SERVER', + 'https://my.communityserver.la/activitypub/review'); + } +} + + + +class ActivitypubReviewIndexDisabledTest extends ActivitypubReviewTestCase { + public function setUp() { + parent::setUp(); + Class_AdminVar::set('FEDERATION_ACTOR_NAME', 'Arcadia'); + + $this->dispatch('/activitypub/review', true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]); + $this->_json = json_decode($this->_response->getBody(), true); + } + + + /** @test */ + public function responseShouldBeServiceUnavailable() { + $this->assertResponseCode(503); + } +} + + + +class ActivitypubReviewIndexEnabledTest extends ActivitypubReviewClientTestCase { + public function setUp() { + parent::setUp(); + Class_AdminVar::set('FEDERATION_ACTOR_NAME', 'Arcadia'); + + $this->dispatch('/activitypub/review', true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]); + $this->_json = json_decode($this->_response->getBody(), true); + } + + + /** @test */ + public function typeShouldBeService() { + $this->assertEquals('Service', $this->_json['type']); + } + + + /** @test */ + public function nameShouldBeArcadia() { + $this->assertEquals('Arcadia', $this->_json['name']); + } + + + /** @test */ + public function outboxShouldBePresent() { + $this->assertContains('outbox', array_keys($this->_json)); + $this->assertContains('/activitypub/review/outbox', $this->_json['outbox']); + } + + + /** @test */ + public function inboxShouldBePresent() { + $this->assertContains('inbox', array_keys($this->_json)); + $this->assertContains('/activitypub/review/outbox', $this->_json['outbox']); + } + + + /** @test */ + public function publicKeyShouldBePresent() { + $this->assertContains('publicKey', array_keys($this->_json)); + $this->assertContains('/activitypub/review/pubkey', $this->_json['publicKey']); + } +} + + + + +class ActivitypubReviewPubkeyDisabledTest extends ActivitypubReviewTestCase { + public function setUp() { + parent::setUp(); + $this->dispatch('/activitypub/review/pubkey', true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]); + $this->_json = json_decode($this->_response->getBody(), true); + } + + + /** @test */ + public function responseShouldBeServiceUnavailable() { + $this->assertResponseCode(503); + } +} + + + + +class ActivitypubReviewPubkeyTest extends ActivitypubReviewClientTestCase { + public function setUp() { + parent::setUp(); + $this->dispatch('/activitypub/review/pubkey', true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]); + $this->_json = json_decode($this->_response->getBody(), true); + } + + + /** @test */ + public function typeShouldBeKey() { + $this->assertEquals('Key', $this->_json['type']); + } + + + /** @test */ + public function idShouldBePresent() { + $this->assertTrue(array_key_exists('id', $this->_json)); + $this->assertContains('/activitypub/review/pubkey', $this->_json['id']); + } + + + /** @test */ + public function ownerShouldBePresent() { + $this->assertTrue(array_key_exists('owner', $this->_json)); + $this->assertContains('/activitypub/review', $this->_json['owner']); + } + + + /** @test */ + public function publicKeyPemShouldBePresent() { + $this->assertTrue(array_key_exists('publicKeyPem', $this->_json)); + $this->assertContains('-----BEGIN PUBLIC KEY-----', + $this->_json['publicKeyPem']); + } + + + /** @test */ + public function federationPubkeyShouldNotBeEmpty() { + $pubkey = Class_AdminVar::get('FEDERATION_PUBKEY'); + $this->assertContains('-----BEGIN PUBLIC KEY-----', $pubkey); + $this->assertContains('-----END PUBLIC KEY-----', $pubkey); + } + + + /** @test */ + public function federationPrivkeyShouldNotBeEmpty() { + $privkey = Class_AdminVar::get('FEDERATION_PRIVKEY'); + $this->assertContains('-----BEGIN RSA PRIVATE KEY-----', $privkey); + $this->assertContains('-----END RSA PRIVATE KEY-----', $privkey); + } +} + + + +class ActivitypubReviewOutboxTest extends ActivitypubReviewClientTestCase { + public function setUp() { + parent::setUp(); + + $client_endpoint = Class_AdminVar::get('FEDERATION_COMMUNITY_SERVER'); + + $this->dispatch('/activitypub/review/outbox', + true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE, + 'Authorization' => 'Bearer ' . $client_endpoint]); + $this->_json = json_decode($this->_response->getBody(), true); + } + + + /** @test */ + public function responseTypeShouldBeCollectionPage() { + $this->assertEquals('CollectionPage', $this->_json['type']); + } +} + + + +class ActivitypubReviewOutboxPageOneTest extends ActivitypubReviewClientTestCase { + public function setUp() { + parent::setUp(); + $this->fixture('Class_AvisNotice', + ['id' => 42, + 'entete' => 'Not so bad', + 'user' => Class_Users::getIdentity(), + 'date_mod' => null, + 'source_author' => null]); + + $client_endpoint = Class_AdminVar::get('FEDERATION_COMMUNITY_SERVER'); + + $this->dispatch('/activitypub/review/outbox/page/1', + true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE, + 'Authorization' => 'Bearer ' . $client_endpoint]); + + $this->_json = json_decode($this->_response->getBody(), true); + } + + + /** @test */ + public function typeShouldBeCollectionPage() { + $this->assertEquals('CollectionPage', $this->_json['type']); + } + + + /** @test */ + public function firstReviewShouldHaveEnteteNotSoBad() { + $this->assertEquals('Not so bad', $this->_json['items'][0]['entete']); + } +} + + + +class ActivitypubReviewOutboxBadFromTest extends ActivitypubReviewClientTestCase { + public function setUp() { + parent::setUp(); + + $client_endpoint = Class_AdminVar::get('FEDERATION_COMMUNITY_SERVER'); + + $this->dispatch('/activitypub/review/outbox?from=no%20valid', + true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE, + 'Authorization' => 'Bearer ' . $client_endpoint]); + $this->_json = json_decode($this->_response->getBody(), true); + } + + + /** @test */ + public function responseShouldBeError400() { + $this->assertResponseCode(400); + } +} + + + +class ActivitypubReviewOutboxValidFromTest extends ActivitypubReviewClientTestCase { + public function setUp() { + parent::setUp(); + + $client_endpoint = Class_AdminVar::get('FEDERATION_COMMUNITY_SERVER'); + + $this->dispatch('/activitypub/review/outbox?from=2019-05-17', + true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE, + 'Authorization' => 'Bearer ' . $client_endpoint]); + $this->_json = json_decode($this->_response->getBody(), true); + } + + + /** @test */ + public function responseShouldBeSuccess200() { + $this->assertResponseCode(200); + } + + + /** @test */ + public function typeShouldBeCollectionPage() { + $this->assertEquals('CollectionPage', $this->_json['type']); + } +} + + + +abstract class ActivitypubReviewServerTestCase extends ActivitypubReviewTestCase { + public function setUp() { + parent::setUp(); + Class_AdminVar::set('FEDERATION_IS_COMMUNITY_SERVER', '1'); + } +} + + + +abstract class ActivitypubReviewInboxJoinTestCase extends ActivitypubReviewServerTestCase { + protected + $_web_client, + $_endpoint, + $_activity; + + public function setUp() { + parent::setUp(); + + $this->_web_client = $this + ->mock() + ->whenCalled('open_url') + ->with('https://my-library-portal.fr/activitypub/review', + ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers('{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Service", + "id": "https://my-library-portal.fr/activitypub/review", + "inbox": "https://my-library-portal.fr/activitypub/review/inbox", + "outbox": "https://my-library-portal.fr/activitypub/review/outbox", + "publicKey": "https://my-library-portal.fr/activitypub/review/pubkey" +}') + + ->whenCalled('open_url') + ->with('https://my-library-portal.fr/activitypub/review/pubkey', + ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers(json_encode(['@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Key', + 'id' => 'https://my-library-portal.fr/activitypub/review/pubkey', + 'owner' => 'https://my-library-portal.fr/activitypub/review', + 'publicKeyPem' => 'TheirKey'])) + + ->beStrict() + ; + + $signer = Storm_Test_ObjectWrapper::on(new Class_HttpSignature('keyId="https://my-library-portal.fr/activitypub/review/pubkey", algorithm="rsa-sha256", headers="(request-target) date digest", signature="TheirSignature"')) + ->whenCalled('sign')->answers('MySignature') + + ->whenCalled('verify') + ->with(['date' => 'Thu, 12 Mar 2015 15:54:35 +0100', + 'digest' => 'MD5=TheirDigest', + '(request-target)' => 'post /activitypub/review/inbox'], + 'TheirKey') + ->answers(true); + + Class_WebService_ActivityPub::setSigner($signer); + Class_WebService_ActivityPub::setTimeSource(new TimeSourceForTest('2019-02-08 16:41:33')); + Class_WebService_ActivityPub::setWebClient($this->_web_client); + Class_WebService_ActivityPub::setThrowErrors(true); + + $actor = (new Service())->name('My library') + ->id('https://my-library-portal.fr/activitypub/review'); + + $this->_endpoint = Class_Url::absolute(['module' => 'activitypub', + 'controller' => 'review'], null, true); + + $object = (new Group())->name($this->_groupName()); + + $join = (new Join) + ->summary('My library followed your reviews') + ->id('https://my-library-portal.fr/activitypub/review/follow/id/42') + ->actor($actor) + ->object($object); + + $this->postDispatchRaw('/activitypub/review/inbox', + (new Stream($join))->render(), + ['Content-Type' => Class_WebService_ActivityPub::MIME_TYPE, + 'Date' => 'Thu, 12 Mar 2015 15:54:35 +0100', + 'Digest' => 'MD5=TheirDigest', + 'Signature' => 'keyId="https://my-library-portal.fr/activitypub/review/pubkey", algorithm="rsa-sha256", headers="(request-target) date digest", signature="TheirSignature"']); + + if ($stream = Stream::fromJson($this->_response->getBody())) + $this->_activity = $stream->getRoot(); + } + + + public function tearDown() { + Class_WebService_ActivityPub::setWebClient(null); + Class_WebService_ActivityPub::setSigner(null); + parent::tearDown(); + } +} + + + + +class ActivitypubReviewInboxJoinDisplayGroupTest extends ActivitypubReviewInboxJoinTestCase { + protected function _groupName() { + return 'REVIEW_DISPLAY'; + } + + + /** @test */ + public function responseShouldBeSuccess() { + $this->assertEquals(200, $this->_response->getHttpResponseCode()); + } + + + /** @test */ + public function shouldBeAccepted() { + $this->assertTrue($this->_activity instanceof Accept); + } + + + /** @test */ + public function actorShouldBeMemberOfReviewDisplayGroup() { + $this->assertNotNull(Class_Federation_GroupMembership::findFirstBy(['actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'group_name' => 'REVIEW_DISPLAY'])); + } +} + + + +class ActivitypubReviewInboxJoinDisplayGroupTwiceTest extends ActivitypubReviewInboxJoinTestCase { + protected function _groupName() { + $this->fixture('Class_Federation_GroupMembership', + ['id' => 44, + 'actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'group_name' => 'REVIEW_DISPLAY']); + + return 'REVIEW_DISPLAY'; + } + + + /** @test */ + public function responseShouldBeSuccess() { + $this->assertEquals(200, $this->_response->getHttpResponseCode()); + } + + + /** @test */ + public function shouldBeAccepted() { + $this->assertTrue($this->_activity instanceof Accept); + } + + + /** @test */ + public function actorShouldBeMemberOfReviewDisplayGroupOnce() { + $this->assertEquals(1, Class_Federation_GroupMembership::countBy(['actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'group_name' => 'REVIEW_DISPLAY'])); + } +} + + + +class ActivitypubReviewInboxJoinUnknownGroupTest extends ActivitypubReviewInboxJoinTestCase { + protected function _groupName() { + return 'ANY_UNKNOWN_GROUP'; + } + + + /** @test */ + public function responseShouldBeSuccess() { + $this->assertEquals(200, $this->_response->getHttpResponseCode()); + } + + + /** @test */ + public function shouldBeRejected() { + $this->assertTrue($this->_activity instanceof Reject); + } + + + /** @test */ + public function actorShouldNotBeMemberOfAnyGroup() { + $this->assertEquals(0, Class_Federation_GroupMembership::countBy(['actor_id' => 'https://my-library-portal.fr/activitypub/review'])); + } +} + + + +class ActivitypubReviewInboxJoinShareGroupTest extends ActivitypubReviewInboxJoinTestCase { + protected function _groupName() { + return 'REVIEW_SHARE'; + } + + + /** @test */ + public function responseShouldBeSuccess() { + $this->assertEquals(200, $this->_response->getHttpResponseCode()); + } + + + /** @test */ + public function shouldBeAccepted() { + $this->assertTrue($this->_activity instanceof Accept); + } + + + /** @test */ + public function actorShouldBeMemberOfReviewDisplayGroup() { + $this->assertNotNull(Class_Federation_GroupMembership::findFirstBy(['actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'group_name' => 'REVIEW_SHARE'])); + } +} + + + +abstract class ActivitypubReviewInboxLeaveTestCase extends ActivitypubReviewServerTestCase { + protected + $_web_client, + $_endpoint, + $_activity; + + public function setUp() { + parent::setUp(); + + $this->fixture('Class_Federation_GroupMembership', + ['id' => 345, + 'actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'group_name' => 'REVIEW_DISPLAY']); + + + $this->fixture('Class_Federation_GroupMembership', + ['id' => 346, + 'actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'group_name' => 'REVIEW_SHARE']); + + $this->fixture('Class_AvisNotice', + ['id' => 55, + 'user' => Class_Users::getIdentity(), + 'CLEF_OEUVRE' => 'UNAMOURDELAPIN44--DAVISJ-', + 'ID_NOTICE' => '113132', + 'DATE_AVIS' => '2019-04-19 17:46:27', + 'DATE_MOD' => 'NULL', + 'NOTE' => '3', + 'ENTETE' => 'Au top', + 'AVIS' => 'comme d\'habitude trop bien !', + 'STATUT' => '0', + 'abon_ou_bib' => '1', + 'USER_KEY' => '0--0--sysadm', + 'flags' => '0', + 'type_doc' => '1', + 'source_actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'source_author' => 'Arcadia', + 'source_primary' => 4839 + ]); + + $this->fixture('Class_Journal', + ['id' => 344, + 'type' => 'AP_REVIEW_HARVEST_346']); + + $this->_web_client = $this + ->mock() + ->whenCalled('open_url') + ->with('https://my-library-portal.fr/activitypub/review', + ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers('{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Service", + "id": "https://my-library-portal.fr/activitypub/review", + "inbox": "https://my-library-portal.fr/activitypub/review/inbox", + "outbox": "https://my-library-portal.fr/activitypub/review/outbox", + "publicKey": "https://my-library-portal.fr/activitypub/review/pubkey" +}') + + ->whenCalled('open_url') + ->with('https://my-library-portal.fr/activitypub/review/pubkey', + ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers(json_encode(['@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Key', + 'id' => 'https://my-library-portal.fr/activitypub/review/pubkey', + 'owner' => 'https://my-library-portal.fr/activitypub/review', + 'publicKeyPem' => 'TheirKey'])) + + ->beStrict() + ; + + $signer = Storm_Test_ObjectWrapper::on(new Class_HttpSignature('keyId="https://my-library-portal.fr/activitypub/review/pubkey", algorithm="rsa-sha256", headers="(request-target) date digest", signature="TheirSignature"')) + ->whenCalled('sign')->answers('MySignature') + + ->whenCalled('verify') + ->with(['date' => 'Thu, 12 Mar 2015 15:54:35 +0100', + 'digest' => 'MD5=TheirDigest', + '(request-target)' => 'post /activitypub/review/inbox'], + 'TheirKey') + ->answers(true); + + Class_WebService_ActivityPub::setSigner($signer); + Class_WebService_ActivityPub::setTimeSource(new TimeSourceForTest('2019-02-08 16:41:33')); + Class_WebService_ActivityPub::setWebClient($this->_web_client); + Class_WebService_ActivityPub::setThrowErrors(true); + + $actor = (new Service())->name('My library') + ->id('https://my-library-portal.fr/activitypub/review'); + + $this->_endpoint = Class_Url::absolute(['module' => 'activitypub', + 'controller' => 'review'], null, true); + + $object = (new Group())->name($this->_groupName()); + + $leave = (new Leave) + ->summary('My library followed your reviews') + ->id('https://my-library-portal.fr/activitypub/review/follow/id/42') + ->actor($actor) + ->object($object); + + $this->postDispatchRaw('/activitypub/review/inbox', + (new Stream($leave))->render(), + ['Content-Type' => Class_WebService_ActivityPub::MIME_TYPE, + 'Date' => 'Thu, 12 Mar 2015 15:54:35 +0100', + 'Digest' => 'MD5=TheirDigest', + 'Signature' => 'keyId="https://my-library-portal.fr/activitypub/review/pubkey", algorithm="rsa-sha256", headers="(request-target) date digest", signature="TheirSignature"']); + + if ($stream = Stream::fromJson($this->_response->getBody())) + $this->_activity = $stream->getRoot(); + } + + + public function tearDown() { + Class_WebService_ActivityPub::setWebClient(null); + Class_WebService_ActivityPub::setSigner(null); + parent::tearDown(); + } +} + + + + +class ActivitypubReviewInboxLeaveDisplayGroupTest extends ActivitypubReviewInboxLeaveTestCase { + protected function _groupName() { + return 'REVIEW_DISPLAY'; + } + + + /** @test */ + public function responseShouldBeSuccess() { + $this->assertEquals(200, $this->_response->getHttpResponseCode()); + } + + + /** @test */ + public function shouldBeAccepted() { + $this->assertTrue($this->_activity instanceof Accept); + } + + + /** @test */ + public function actorShouldBeNoLongerMemberOfReviewDisplayGroup() { + $this->assertNull(Class_Federation_GroupMembership::findFirstBy(['actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'group_name' => 'REVIEW_DISPLAY'])); + } +} + + + + +class ActivitypubReviewInboxLeaveShareGroupTest extends ActivitypubReviewInboxLeaveTestCase { + protected function _groupName() { + return 'REVIEW_SHARE'; + } + + + /** @test */ + public function responseShouldBeSuccess() { + $this->assertEquals(200, $this->_response->getHttpResponseCode()); + } + + + /** @test */ + public function shouldBeAccepted() { + $this->assertTrue($this->_activity instanceof Accept); + } + + + /** @test */ + public function actorShouldBeNoLongerMemberOfReviewShareGroup() { + $this->assertNull(Class_Federation_GroupMembership::findFirstBy(['actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'group_name' => 'REVIEW_SHARE'])); + } + + + /** @test */ + public function sharedReviewsShouldBeRemoved() { + $this->assertNull(Class_AvisNotice::findFirstBy(['source_actor_id' => 'https://my-library-portal.fr/activitypub/review'])); + } + + + /** @test */ + public function harvestJournalShouldBeRemoved() { + $this->assertNull(Class_Journal::findFirstBy(['type' => 'AP_REVIEW_HARVEST_346'])); + } +} + + + + +class ActivitypubReviewInboxLeaveUnkownGroupTest extends ActivitypubReviewInboxLeaveTestCase { + protected function _groupName() { + return 'I_M_UNKNOWN'; + } + + + /** @test */ + public function responseShouldBeSuccess() { + $this->assertEquals(200, $this->_response->getHttpResponseCode()); + } + + + /** @test */ + public function shouldBeRejected() { + $this->assertTrue($this->_activity instanceof Reject); + } + + + /** @test */ + public function actorShouldStillBeMemberOfReviewDisplayGroup() { + $this->assertNotNull(Class_Federation_GroupMembership::findFirstBy(['actor_id' => 'https://my-library-portal.fr/activitypub/review', + 'group_name' => 'REVIEW_DISPLAY'])); + } +} + + + +class ActivitypubReviewOutboxForRecordTest extends ActivitypubReviewServerTestCase { + public function setUp() { + parent::setUp(); + + $this->fixture('Class_AvisNotice', + ['id' => 55, + 'user' => Class_Users::getIdentity(), + 'CLEF_OEUVRE' => 'UNAMOURDELAPIN44--DAVISJ-', + 'ID_NOTICE' => '113132', + 'DATE_AVIS' => '2019-04-19 17:46:27', + 'DATE_MOD' => 'NULL', + 'NOTE' => '3', + 'ENTETE' => 'Au top', + 'AVIS' => 'comme d\'habitude trop bien !', + 'STATUT' => '0', + 'abon_ou_bib' => '1', + 'USER_KEY' => '0--0--sysadm', + 'flags' => '0', + 'type_doc' => '1', + 'source_actor_id' => 'https://other.bokeh.es/activitypub/review', + 'source_author' => 'Arcadia', + 'source_primary' => 4839 + ]); + + $client_endpoint = 'https://my.super_bokeh.nl/activitypub/review'; + + $this->fixture('Class_Federation_GroupMembership', + ['id' => 1, + 'actor_id' => $client_endpoint, + 'group_name' => 'REVIEW_DISPLAY']); + + $this->dispatch('/activitypub/review/outbox?key=UNAMOURDELAPIN44--DAVISJ-&page=1', + true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE, + 'Authorization' => 'Bearer ' . $client_endpoint]); + + $this->_json = json_decode($this->_response->getBody(), true); + } + + + /** @test */ + public function responseShouldBeACollectionPage() { + $this->assertEquals('CollectionPage', $this->_json['type']); + } + + + /** @test */ + public function firstReviewTitleShouldBeAuTop() { + $this->assertEquals('Au top', $this->_json['items'][0]['entete']); + } + + + /** @test */ + public function firstReviewAuthorShouldBeArcadia() { + $this->assertEquals('Arcadia', $this->_json['items'][0]['source_author']); + } +} + + + +class ActivitypubReviewOutboxHarvestingTest extends ActivitypubReviewClientTestCase { + protected $_review; + + public function setUp() { + parent::setUp(); + + $this->fixture('Class_AvisNotice', + ['id' => 55, + 'user' => Class_Users::getIdentity(), + 'CLEF_OEUVRE' => 'UNAMOURDELAPIN44--DAVISJ-', + 'ID_NOTICE' => '113132', + 'DATE_AVIS' => '2019-04-19 17:46:27', + 'DATE_MOD' => 'NULL', + 'NOTE' => '3', + 'ENTETE' => 'Au top', + 'AVIS' => 'comme d\'habitude trop bien !', + 'STATUT' => '0', + 'USER_KEY' => '0--0--sysadm', + 'flags' => '0', + 'type_doc' => '1', + 'source_author' => null, + ]); + + $client_endpoint = Class_AdminVar::get('FEDERATION_COMMUNITY_SERVER'); + + $this->dispatch('/activitypub/review/outbox?from=&page=1', + true, + ['Accept' => Class_WebService_ActivityPub::MIME_TYPE, + 'Authorization' => 'Bearer ' . $client_endpoint]); + + $this->_json = json_decode($this->_response->getBody(), true); + $this->_review = $this->_json['items'][0]; + } + + + /** @test */ + public function responseTypeShouldBeCollectionPage() { + $this->assertEquals('CollectionPage', $this->_json['type']); + } + + + /** @test */ + public function collectionShouldHaveOneItemInTotal() { + $this->assertEquals(1, $this->_json['totalItems']); + } + + + /** @test */ + public function reviewIdShouldBe55() { + $this->assertEquals(55, $this->_review['id']); + } + + + /** @test */ + public function reviewClefOeuvreShouldBeUnAmourDeLapin() { + $this->assertEquals('UNAMOURDELAPIN44--DAVISJ-', $this->_review['clef_oeuvre']); + } + + + /** @test */ + public function reviewDateShouldBe2019_04_19() { + $this->assertEquals('2019-04-19 17:46:27', $this->_review['date_avis']); + } + + + /** @test */ + public function reviewShouldHaveBeenRated3() { + $this->assertEquals(3, $this->_review['note']); + } + + + /** @test */ + public function reviewEnteteShouldBeAuTop() { + $this->assertEquals('Au top', $this->_review['entete']); + } + + + /** @test */ + public function reviewContentShouldBeCommeDHabitude() { + $this->assertEquals('comme d\'habitude trop bien !', $this->_review['avis']); + } + + + public function filteredFields() { + return array_map(function($item) { return [$item]; }, + ['id_user', + 'id_notice', + 'statut', + 'user_key', + 'flags', + 'type_doc', + 'source_actor_id', + 'source_primary']); + } + + + /** @test @dataProvider filteredFields */ + public function reviewShouldNotHaveFilteredField($field) { + $this->assertNotContains($field, array_keys($this->_review)); + $this->assertNotContains(strtoupper($field), array_keys($this->_review)); + } +} diff --git a/tests/scenarios/Activitypub/FederationReviewBatchTest.php b/tests/scenarios/Activitypub/FederationReviewBatchTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cffa410cdc927af4a1ba312a11ff9ad8df4130b0 --- /dev/null +++ b/tests/scenarios/Activitypub/FederationReviewBatchTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright (c) 2012-2019, 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 ActivitypubAdminBatchControllerTest extends Admin_AbstractControllerTestCase { + protected $_storm_default_to_volatile = true; + + /** @test */ + public function withServerDisabledFederationBatchShouldNotBeAvailable() { + Class_AdminVar::set('FEDERATION_IS_COMMUNITY_SERVER', '0'); + $this->dispatch('/admin/batch'); + $this->assertNotXPath('//a[contains(@href, "FEDERATION_REVIEW_HARVEST")]'); + } + + + /** @test */ + public function withServerEnabledFederationBatchShouldBeAvailable() { + Class_AdminVar::set('FEDERATION_IS_COMMUNITY_SERVER', '1'); + $this->dispatch('/admin/batch'); + $this->assertXPath('//a[contains(@href, "FEDERATION_REVIEW_HARVEST")]'); + } +} + + + +class ActivitypubFederationReviewBatchTest extends ModelTestCase { + protected + $_actor_id, + $_get_response_called = false, + $_web_client; + + public function setUp() { + parent::setUp(); + $this->_actor_id = 'https://review-share.member.com/activitypub/review'; + + $this->fixture('Class_Federation_GroupMembership', + ['id' => 1, + 'group_name' => 'REVIEW_SHARE', + 'actor_id' => $this->_actor_id, + 'accepted_at' => '2019-05-03 14:15:54']); + + $this->_web_client = $this + ->mock() + ->whenCalled('open_url') + ->with($this->_actor_id, ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers(str_replace('{actor}', + $this->_actor_id, + '{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Service", + "id": "{actor}", + "name": "Arcadia", + "inbox": "{actor}/inbox", + "outbox": "{actor}/outbox", + "publicKey": "{actor}/pubkey" +}')) + + ->whenCalled('open_url') + ->with($this->_actor_id . '/pubkey', + ['headers' => ['Accept' => Class_WebService_ActivityPub::MIME_TYPE]]) + ->answers(str_replace('{actor}', + $this->_actor_id, + '{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Key", + "id": "{actor}/pubkey", + "owner": "{actor}", + "publicKeyPem": "TheirKey" +}')) + + ->whenCalled('getResponse') + ->willDo(function() + { + if ($this->_get_response_called) + return $this->_responseWithItems([]); + + $this->_get_response_called = true; + return $this->_responseWithItems([['id' => '3773', + 'clef_oeuvre' => 'UNAMOURDELAPIN44--DAVISJ-', + 'date_avis' => '2019-04-19 17:46:27', + 'date_mod' => null, + 'note' => '3', + 'entete' => 'Au top', + 'avis' => 'Comme d\'habitude, trop bien !', + 'source_author' => 'SuperBibliothécaire']]); + }) + ; + + $signer = Storm_Test_ObjectWrapper::on(new Class_HttpSignature('keyId="'. $this->_actor_id .'/pubkey", algorithm="rsa-sha256", headers="(request-target) date digest", signature="TheirSignature"')) + ->whenCalled('sign')->answers('MySignature') + ->whenCalled('verify')->answers(true); + + Class_WebService_ActivityPub::setSigner($signer); + Class_WebService_ActivityPub::setTimeSource(new TimeSourceForTest('2019-05-03 16:10:35')); + Class_WebService_ActivityPub::setWebClient($this->_web_client); + Class_WebService_ActivityPub::setThrowErrors(true); + + (new Class_Batch_FederationReviewHarvest)->run(); + } + + + protected function _responseWithItems($items) { + return $this->mock() + ->whenCalled('isError')->answers(false) + ->whenCalled('getHeader')->with('Signature')->answers('TheirSignature') + ->whenCalled('getHeaders')->answers([]) + ->whenCalled('getBody') + ->answers(json_encode(['@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'CollectionPage', + 'totalItems' => count($items), + 'items' => $items])); + } + + + /** @test */ + public function shouldHaveAReviewWithActorIdAndAuthor() { + $review = Class_AvisNotice::findFirstBy(['source_actor_id' => $this->_actor_id, + 'source_author' => 'Arcadia']); + $this->assertNotNull($review); + } +}