]> git.agnieray.net Git - galette.git/blobdiff - galette/lib/Galette/Controllers/Crud/MembersController.php
Do not override members list filters on member display or edit; closes #1803
[galette.git] / galette / lib / Galette / Controllers / Crud / MembersController.php
index a1a02f3a877e31b8f1a275e3a86ebe4e2813ff83..8ee229f58904c249c45e6a2073eed11ae06c5e18 100644 (file)
@@ -7,7 +7,7 @@
  *
  * PHP version 5
  *
- * Copyright © 2019-2020 The Galette Team
+ * Copyright © 2019-2023 The Galette Team
  *
  * This file is part of Galette (http://galette.tuxfamily.org).
  *
@@ -28,9 +28,8 @@
  * @package   Galette
  *
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2019-2020 The Galette Team
+ * @copyright 2019-2023 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
- * @version   SVN: $Id$
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.9.4dev - 2019-12-02
  */
 namespace Galette\Controllers\Crud;
 
 use Galette\Controllers\CrudController;
-
-use Slim\Http\Request;
-use Slim\Http\Response;
-use Galette\Core\Authentication;
+use Galette\DynamicFields\Boolean;
+use Galette\Features\BatchList;
+use Slim\Psr7\Request;
+use Slim\Psr7\Response;
 use Galette\Core\GaletteMail;
-use Galette\Core\PasswordImage;
-use Galette\Core\Picture;
+use Galette\Core\Gaptcha;
 use Galette\Entity\Adherent;
 use Galette\Entity\Contribution;
 use Galette\Entity\ContributionsTypes;
@@ -52,11 +50,9 @@ use Galette\Entity\DynamicFieldsHandle;
 use Galette\Entity\Group;
 use Galette\Entity\Status;
 use Galette\Entity\FieldsConfig;
-use Galette\Entity\Texts;
+use Galette\Entity\Social;
 use Galette\Filters\AdvancedMembersList;
 use Galette\Filters\MembersList;
-use Galette\IO\File;
-use Galette\IO\MembersCsv;
 use Galette\Repository\Groups;
 use Galette\Repository\Members;
 use Galette\Repository\PaymentTypes;
@@ -70,7 +66,7 @@ use Analog\Analog;
  * @name      GaletteController
  * @package   Galette
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2019-2020 The Galette Team
+ * @copyright 2019-2023 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.9.4dev - 2019-12-02
@@ -78,6 +74,11 @@ use Analog\Analog;
 
 class MembersController extends CrudController
 {
+    use BatchList;
+
+    /** @var bool */
+    private $is_self_membership = false;
+
     // CRUD - Create
 
     /**
@@ -85,13 +86,25 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
      *
      * @return Response
      */
-    public function add(Request $request, Response $response, array $args = []) :Response
+    public function add(Request $request, Response $response): Response
+    {
+        return $this->edit($request, $response, null, 'add');
+    }
+
+    /**
+     * Add child page
+     *
+     * @param Request  $request  PSR Request
+     * @param Response $response PSR Response
+     *
+     * @return Response
+     */
+    public function addChild(Request $request, Response $response): Response
     {
-        return $this->edit($request, $response, $args);
+        return $this->edit($request, $response, null, 'addchild');
     }
 
     /**
@@ -99,26 +112,23 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
      *
      * @return Response
      */
-    public function selfSubscribe(Request $request, Response $response, array $args = []) :Response
+    public function selfSubscribe(Request $request, Response $response): Response
     {
         if (!$this->preferences->pref_bool_selfsubscribe || $this->login->isLogged()) {
             return $response
                 ->withStatus(301)
-                ->withHeader('Location', $this->router->pathFor('slash'));
+                ->withHeader('Location', $this->routeparser->urlFor('slash'));
         }
 
         if ($this->session->member !== null) {
             $member = $this->session->member;
             $this->session->member = null;
         } else {
-            $deps = [
-                'dynamics'  => true
-            ];
-            $member = new Adherent($this->zdb, null, $deps);
+            $member = new Adherent($this->zdb);
+            $member->enableDep('dynamics');
         }
 
         //mark as self membership
@@ -128,45 +138,48 @@ class MembersController extends CrudController
         $fc = $this->fields_config;
         $form_elements = $fc->getFormElements($this->login, true, true);
 
-        //image to defeat mass filling forms
-        $spam = new PasswordImage();
-        $spam_pass = $spam->newImage();
-        $spam_img = $spam->getImage();
-
         // members
         $m = new Members();
-        $members = $m->getSelectizedMembers(
+        $members = $m->getDropdownMembers(
             $this->zdb,
+            $this->login,
             $member->hasParent() ? $member->parent->id : null
         );
 
-        $params['members'] = [
-            'filters'   => $m->getFilters(),
-            'count'     => $m->getCount()
+        $params = [
+            'members' => [
+                'filters'   => $m->getFilters(),
+                'count'     => $m->getCount()
+            ]
         ];
 
         if (count($members)) {
             $params['members']['list'] = $members;
         }
 
+        $gaptcha = new Gaptcha($this->i18n);
+        $this->session->gaptcha = $gaptcha;
+
+        $titles = new Titles($this->zdb);
+
         // display page
         $this->view->render(
             $response,
-            'member.tpl',
+            'pages/member_form.html.twig',
             array(
                 'page_title'        => _T("Subscription"),
-                'parent_tpl'        => 'public_page.tpl',
+                'parent_tpl'        => 'public_page.html.twig',
                 'member'            => $member,
                 'self_adh'          => true,
                 'autocomplete'      => true,
+                'osocials'          => new Social($this->zdb),
                 // pseudo random int
                 'time'              => time(),
-                'titles_list'       => Titles::getList($this->zdb),
-                //self_adh specific
-                'spam_pass'         => $spam_pass,
-                'spam_img'          => $spam_img,
+                'titles_list'       => $titles->getList(),
                 'fieldsets'         => $form_elements['fieldsets'],
-                'hidden_elements'   => $form_elements['hiddens']
+                'hidden_elements'   => $form_elements['hiddens'],
+                //self_adh specific
+                'gaptcha'           => $gaptcha
             ) + $params
         );
         return $response;
@@ -177,28 +190,41 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
      *
      * @return Response
      */
-    public function doAdd(Request $request, Response $response, array $args = []) :Response
+    public function doAdd(Request $request, Response $response): Response
+    {
+        return $this->store($request, $response);
+    }
+
+    /**
+     * Self subscription add action
+     *
+     * @param Request  $request  PSR Request
+     * @param Response $response PSR Response
+     *
+     * @return Response
+     */
+    public function doSelfSubscribe(Request $request, Response $response): Response
     {
-        return $this->store($request, $response, $args);
+        $this->setSelfMembership();
+        return $this->doAdd($request, $response);
     }
 
+
     /**
      * Duplicate action
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param integer  $id_adh   Member ID to duplicate
      *
      * @return Response
      */
-    public function duplicate(Request $request, Response $response, array $args = []) :Response
+    public function duplicate(Request $request, Response $response, int $id_adh): Response
     {
-        $id_adh = (int)$args[Adherent::PK];
-        $adh = new Adherent($this->zdb, $id_adh, ['dynamics' => true]);
+        $adh = new Adherent($this->zdb, $id_adh, ['dynamics' => true, 'parent' => true]);
         $adh->setDuplicate();
 
         //store entity in session
@@ -206,7 +232,7 @@ class MembersController extends CrudController
 
         return $response
             ->withStatus(301)
-            ->withHeader('Location', $this->router->pathFor('editmember', ['action' => 'add']));
+            ->withHeader('Location', $this->routeparser->urlFor('addMember'));
     }
 
     // /CRUD - Create
@@ -217,25 +243,18 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param integer  $id       Member ID
      *
      * @return Response
      */
-    public function show(Request $request, Response $response, array $args) :Response
+    public function show(Request $request, Response $response, int $id): Response
     {
-        $id = (int)$args['id'];
-
-        $deps = array(
-            'picture'   => true,
-            'groups'    => true,
-            'dues'      => true,
-            'parent'    => true,
-            'children'  => true,
-            'dynamics'  => true
-        );
-        $member = new Adherent($this->zdb, $id, $deps);
+        $member = new Adherent($this->zdb);
+        $member
+            ->enableAllDeps()
+            ->load($id);
 
-        if (!$member->canEdit($this->login)) {
+        if (!$member->canShow($this->login)) {
             $this->flash->addMessage(
                 'error_detected',
                 _T("You do not have permission for requested URL.")
@@ -245,22 +264,21 @@ class MembersController extends CrudController
                 ->withStatus(301)
                 ->withHeader(
                     'Location',
-                    $this->router->pathFor('me')
+                    $this->routeparser->urlFor('me')
                 );
         }
 
         if ($member->id == null) {
-            //member does not exists!
+            //member does not exist!
             $this->flash->addMessage(
                 'error_detected',
-                str_replace('%id', $args['id'], _T("No member #%id."))
+                str_replace('%id', $id, _T("No member #%id."))
             );
 
             return $response
-                ->withStatus(404)
                 ->withHeader(
                     'Location',
-                    $this->router->pathFor('slash')
+                    $this->routeparser->urlFor('slash')
                 );
         }
 
@@ -271,7 +289,7 @@ class MembersController extends CrudController
         // display page
         $this->view->render(
             $response,
-            'voir_adherent.tpl',
+            'pages/member_show.html.twig',
             array(
                 'page_title'        => _T("Member Profile"),
                 'member'            => $member,
@@ -279,7 +297,9 @@ class MembersController extends CrudController
                 'pref_card_self'    => $this->preferences->pref_card_self,
                 'groups'            => Groups::getSimpleList(),
                 'time'              => time(),
-                'display_elements'  => $display_elements
+                'display_elements'  => $display_elements,
+                'osocials'          => new Social($this->zdb),
+                'navigate'          => $this->handleNavigationLinks($member->id)
             )
         );
         return $response;
@@ -290,44 +310,38 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
      *
      * @return Response
      */
-    public function showMe(Request $request, Response $response, array $args = []) :Response
+    public function showMe(Request $request, Response $response): Response
     {
         if ($this->login->isSuperAdmin()) {
             return $response
                 ->withStatus(301)
-                ->withHeader('Location', $this->router->pathFor('slash'));
+                ->withHeader('Location', $this->routeparser->urlFor('slash'));
         }
-        $args['show_me'] = true;
-        $args['id'] = $this->login->id;
-        return $this->show($request, $response, $args);
+        return $this->show($request, $response, $this->login->id);
     }
 
     /**
      * Public pages (trombinoscope, public list)
      *
-     * @param Request  $request  PSR Request
-     * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param Request        $request  PSR Request
+     * @param Response       $response PSR Response
+     * @param string         $option   One of 'page' or 'order'
+     * @param string|integer $value    Value of the option
+     * @param string         $type     List type (either list or trombi)
      *
      * @return Response
      */
-    public function publicList(Request $request, Response $response, array $args = []) :Response
-    {
-        $option = null;
-        $type = $args['type'];
-        if (isset($args['option'])) {
-            $option = $args['option'];
-        }
-        $value = null;
-        if (isset($args['value'])) {
-            $value = $args['value'];
-        }
-
-        $varname = 'public_filter_' . $type;
+    public function publicList(
+        Request $request,
+        Response $response,
+        $option = null,
+        $value = null,
+        $type = null
+    ): Response {
+        $varname = $this->getFilterName(['prefix' => 'public', 'suffix' => $type]);
         if (isset($this->session->$varname)) {
             $filters = $this->session->$varname;
         } else {
@@ -351,19 +365,21 @@ class MembersController extends CrudController
         $this->session->$varname = $filters;
 
         //assign pagination variables to the template and add pagination links
-        $filters->setSmartyPagination($this->router, $this->view->getSmarty(), false);
+        $filters->setViewPagination($this->routeparser, $this->view, false);
 
         // display page
         $this->view->render(
             $response,
-            ($type === 'list' ? 'liste_membres' : 'trombinoscope') . '.tpl',
+            ($type === 'list' ? 'pages/members_public_list' : 'pages/members_public_gallery') . '.html.twig',
             array(
                 'page_title'    => ($type === 'list' ? _T("Members list") : _T('Trombinoscope')),
                 'additionnal_html_class'    => ($type === 'list' ? '' : 'trombinoscope'),
                 'type'          => $type,
                 'members'       => $members,
                 'nb_members'    => $m->getCount(),
-                'filters'       => $filters
+                'filters'       => $filters,
+                // pseudo random int
+                'time'              => time(),
             )
         );
         return $response;
@@ -374,16 +390,15 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param string   $type     Type
      *
      * @return Response
      */
-    public function filterPublicList(Request $request, Response $response, array $args = []) :Response
+    public function filterPublicList(Request $request, Response $response, string $type): Response
     {
-        $type = $args['type'];
         $post = $request->getParsedBody();
 
-        $varname = 'public_filter_' . $type;
+        $varname = $this->getFilterName(['prefix' => 'public', 'suffix' => $type]);
         if (isset($this->session->$varname)) {
             $filters = $this->session->$varname;
         } else {
@@ -396,7 +411,7 @@ class MembersController extends CrudController
         } else {
             //number of rows to show
             if (isset($post['nbshow'])) {
-                $filters->show = $post['nbshow'];
+                $filters->show = (int)$post['nbshow'];
             }
         }
 
@@ -404,118 +419,23 @@ class MembersController extends CrudController
 
         return $response
             ->withStatus(301)
-            ->withHeader('Location', $this->router->pathFor('publicList', ['type' => $type]));
-    }
-
-    /**
-     * Get a dynamic file
-     *
-     * @param Request  $request  PSR Request
-     * @param Response $response PSR Response
-     * @param array    $args     Request arguments
-     *
-     * @return Response
-     */
-    public function getDynamicFile(Request $request, Response $response, array $args) :Response
-    {
-        $id = (int)$args['id'];
-        $deps = array(
-            'picture'   => false,
-            'groups'    => false,
-            'dues'      => false,
-            'parent'    => false,
-            'children'  => false,
-            'dynamics'  => true
-        );
-        $member = new Adherent($this->zdb, $id, $deps);
-
-        $denied = null;
-        if (!$member->canEdit($this->login)) {
-            $fields = $member->getDynamicFields()->getFields();
-            if (!isset($fields[$args['fid']])) {
-                //field does not exists or access is forbidden
-                $denied = true;
-            } else {
-                $denied = false;
-            }
-        }
-
-        if ($denied === true) {
-            $this->flash->addMessage(
-                'error_detected',
-                _T("You do not have permission for requested URL.")
-            );
-
-            return $response
-                ->withStatus(403)
-                ->withHeader(
-                    'Location',
-                    $this->router->pathFor(
-                        'member',
-                        ['id' => $id]
-                    )
-                );
-        }
-
-        $filename = str_replace(
-            [
-                '%mid',
-                '%fid',
-                '%pos'
-            ],
-            [
-                $args['id'],
-                $args['fid'],
-                $args['pos']
-            ],
-            'member_%mid_field_%fid_value_%pos'
-        );
-
-        if (file_exists(GALETTE_FILES_PATH . $filename)) {
-            $type = File::getMimeType(GALETTE_FILES_PATH . $filename);
-            $response = $response
-                ->withHeader('Content-Type', $type)
-                ->withHeader('Content-Disposition', 'attachment;filename="' . $args['name'] . '"')
-                ->withHeader('Pragma', 'no-cache');
-            $response->write(readfile(GALETTE_FILES_PATH . $filename));
-            return $response;
-        } else {
-            Analog::log(
-                'A request has been made to get an exported file named `' .
-                $filename .'` that does not exists.',
-                Analog::WARNING
-            );
-
-            $this->flash->addMessage(
-                'error_detected',
-                _T("The file does not exists or cannot be read :(")
-            );
-
-            return $response
-                ->withStatus(404)
-                ->withHeader(
-                    'Location',
-                    $this->router->pathFor('member', ['id' => $args['id']])
-                );
-        }
+            ->withHeader('Location', $this->routeparser->urlFor('publicList', ['type' => $type]));
     }
 
     /**
      * Members list
      *
-     * @param Request  $request  PSR Request
-     * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param Request        $request  PSR Request
+     * @param Response       $response PSR Response
+     * @param string         $option   One of 'page' or 'order'
+     * @param string|integer $value    Value of the option
      *
      * @return Response
      */
-    public function list(Request $request, Response $response, array $args = []) :Response
+    public function list(Request $request, Response $response, $option = null, $value = null): Response
     {
-        $option = $args['option'] ?? null;
-        $value = $args['value'] ?? null;
-
-        if (isset($this->session->filter_members)) {
-            $filters = $this->session->filter_members;
+        if (isset($this->session->{$this->getFilterName()})) {
+            $filters = $this->session->{$this->getFilterName()};
         } else {
             $filters = new MembersList();
         }
@@ -544,15 +464,15 @@ class MembersController extends CrudController
         $groups_list = $groups->getList();
 
         //assign pagination variables to the template and add pagination links
-        $filters->setSmartyPagination($this->router, $this->view->getSmarty(), false);
-        $filters->setViewCommonsFilters($this->preferences, $this->view->getSmarty());
+        $filters->setViewPagination($this->routeparser, $this->view, false);
+        $filters->setViewCommonsFilters($this->preferences, $this->view);
 
-        $this->session->filter_members = $filters;
+        $this->session->{$this->getFilterName()} = $filters;
 
         // display page
         $this->view->render(
             $response,
-            'gestion_adherents.tpl',
+            'pages/members_list.html.twig',
             array(
                 'page_title'            => _T("Members management"),
                 'require_mass'          => true,
@@ -560,7 +480,8 @@ class MembersController extends CrudController
                 'filter_groups_options' => $groups_list,
                 'nb_members'            => $members->getCount(),
                 'filters'               => $filters,
-                'adv_filters'           => $filters instanceof AdvancedMembersList
+                'adv_filters'           => $filters instanceof AdvancedMembersList,
+                'galette_list'          => $this->lists_config->getDisplayElements($this->login)
             )
         );
         return $response;
@@ -574,30 +495,25 @@ class MembersController extends CrudController
      *
      * @return Response
      */
-    public function filter(Request $request, Response $response) :Response
+    public function filter(Request $request, Response $response)Response
     {
         $post = $request->getParsedBody();
-        if (isset($this->session->filter_members)) {
-            //CAUTION: this one may be simple or advanced, display must change
-            $filters = $this->session->filter_members;
-        } else {
-            $filters = new MembersList();
-        }
+        $filters = $this->session->{$this->getFilterName()} ?? new MembersList();
 
-        //reintialize filters
+        //reinitialize filters
         if (isset($post['clear_filter'])) {
             $filters = new MembersList();
         } elseif (isset($post['clear_adv_filter'])) {
-            $this->session->filter_members = null;
-            unset($this->session->filter_members);
+            $this->session->{$this->getFilterName()} = null;
+            unset($this->session->{$this->getFilterName()});
 
             return $response
                 ->withStatus(301)
-                ->withHeader('Location', $this->router->pathFor('advanced-search'));
+                ->withHeader('Location', $this->routeparser->urlFor('advanced-search'));
         } elseif (isset($post['adv_criteria'])) {
             return $response
                 ->withStatus(301)
-                ->withHeader('Location', $this->router->pathFor('advanced-search'));
+                ->withHeader('Location', $this->routeparser->urlFor('advanced-search'));
         } else {
             //string to filter
             if (isset($post['filter_str'])) { //filter search string
@@ -629,14 +545,15 @@ class MembersController extends CrudController
                 $filters->email_filter = (int)$post['email_filter'];
             }
             //group filter
-            if (isset($post['group_filter'])
+            if (
+                isset($post['group_filter'])
                 && $post['group_filter'] > 0
             ) {
                 $filters->group_filter = (int)$post['group_filter'];
             }
             //number of rows to show
             if (isset($post['nbshow'])) {
-                $filters->show = $post['nbshow'];
+                $filters->show = (int)$post['nbshow'];
             }
 
             if (isset($post['advanced_filtering'])) {
@@ -652,10 +569,11 @@ class MembersController extends CrudController
                         if (!$freed) {
                             $i = 0;
                             foreach ($post['free_field'] as $f) {
-                                if (trim($f) !== ''
+                                if (
+                                    trim($f) !== ''
                                     && trim($post['free_text'][$i]) !== ''
                                 ) {
-                                    $fs_search = $post['free_text'][$i];
+                                    $fs_search = htmlspecialchars($post['free_text'][$i], ENT_QUOTES);
                                     $log_op
                                         = (int)$post['free_logical_operator'][$i];
                                     $qry_op
@@ -710,18 +628,18 @@ class MembersController extends CrudController
                 ->withStatus(301)
                 ->withHeader(
                     'Location',
-                    $this->router->pathFor(
+                    $this->routeparser->urlFor(
                         'saveSearch',
                         $post
                     )
                 );
         }
 
-        $this->session->filter_members = $filters;
+        $this->session->{$this->getFilterName()} = $filters;
 
         return $response
             ->withStatus(301)
-            ->withHeader('Location', $this->router->pathFor('members'));
+            ->withHeader('Location', $this->routeparser->urlFor('members'));
     }
 
     /**
@@ -732,10 +650,10 @@ class MembersController extends CrudController
      *
      * @return Response
      */
-    public function advancedSearch(Request $request, Response $response) :Response
+    public function advancedSearch(Request $request, Response $response)Response
     {
-        if (isset($this->session->filter_members)) {
-            $filters = $this->session->filter_members;
+        if (isset($this->session->{$this->getFilterName()})) {
+            $filters = $this->session->{$this->getFilterName()};
             if (!$filters instanceof AdvancedMembersList) {
                 $filters = new AdvancedMembersList($filters);
             }
@@ -746,45 +664,17 @@ class MembersController extends CrudController
         $groups = new Groups($this->zdb, $this->login);
         $groups_list = $groups->getList();
 
-        //we want only visibles fields
+        //we want only visible fields
         $fields = $this->members_fields;
         $fc = $this->fields_config;
-        $visibles = $fc->getVisibilities();
-        $access_level = $this->login->getAccessLevel();
-
-        //remove not searchable fields
-        unset($fields['mdp_adh']);
-
-        foreach ($fields as $k => $f) {
-            if ($visibles[$k] == FieldsConfig::NOBODY ||
-                ($visibles[$k] == FieldsConfig::ADMIN &&
-                    $access_level < Authentication::ACCESS_ADMIN) ||
-                ($visibles[$k] == FieldsConfig::STAFF &&
-                    $access_level < Authentication::ACCESS_STAFF) ||
-                ($visibles[$k] == FieldsConfig::MANAGER &&
-                    $access_level < Authentication::ACCESS_MANAGER)
-            ) {
-                unset($fields[$k]);
-            }
-        }
-
-        //add status label search
-        if ($pos = array_search(Status::PK, array_keys($fields))) {
-            $fields = array_slice($fields, 0, $pos, true) +
-                ['status_label'  => ['label' => _T('Status label')]] +
-                array_slice($fields, $pos, count($fields) -1, true);
-        }
+        $fc->filterVisible($this->login, $fields);
 
         //dynamic fields
-        $deps = array(
-            'picture'   => false,
-            'groups'    => false,
-            'dues'      => false,
-            'parent'    => false,
-            'children'  => false,
-            'dynamics'  => false
-        );
-        $member = new Adherent($this->zdb, $this->login->login, $deps);
+        $member = new Adherent($this->zdb);
+        $member
+            ->disableAllDeps()
+            ->enableDep('dynamics')
+            ->loadFromLoginOrMail($this->login->login);
         $adh_dynamics = new DynamicFieldsHandle($this->zdb, $this->login, $member);
 
         $contrib = new Contribution($this->zdb, $this->login);
@@ -796,7 +686,7 @@ class MembersController extends CrudController
         //Contributions types
         $ct = new ContributionsTypes($this->zdb);
 
-        //Payments types
+        //Payment types
         $ptypes = new PaymentTypes(
             $this->zdb,
             $this->preferences,
@@ -804,18 +694,26 @@ class MembersController extends CrudController
         );
         $ptlist = $ptypes->getList();
 
-        $filters->setViewCommonsFilters($this->preferences, $this->view->getSmarty());
+        $filters->setViewCommonsFilters($this->preferences, $this->view);
+
+        $social = new Social($this->zdb);
+        $types = $member->getMemberRegisteredTypes();
+        $social_types = [];
+        foreach ($types as $type) {
+            $social_types[$type] = $social->getSystemType($type);
+        }
 
         // display page
         $this->view->render(
             $response,
-            'advanced_search.tpl',
+            'pages/advanced_search.html.twig',
             array(
                 'page_title'            => _T("Advanced search"),
                 'filter_groups_options' => $groups_list,
                 'search_fields'         => $fields,
-                'adh_dynamics'          => $adh_dynamics->getFields(),
-                'contrib_dynamics'      => $contrib_dynamics->getFields(),
+                'adh_dynamics'          => $adh_dynamics->getSearchFields(),
+                'contrib_dynamics'      => $contrib_dynamics->getSearchFields(),
+                'adh_socials'           => $social_types,
                 'statuts'               => $statuts->getList(),
                 'contributions_types'   => $ct->getList(),
                 'filters'               => $filters,
@@ -828,29 +726,26 @@ class MembersController extends CrudController
     /**
      * Members list for ajax
      *
-     * @param Request  $request  PSR Request
-     * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param Request        $request  PSR Request
+     * @param Response       $response PSR Response
+     * @param string|null    $option   One of 'page' or 'order'
+     * @param string|integer $value    Value of the option
      *
      * @return Response
      */
-    public function ajaxList(Request $request, Response $response, array $args = []) :Response
+    public function ajaxList(Request $request, Response $response, string $option = null, $value = null): Response
     {
         $post = $request->getParsedBody();
 
-        if (isset($this->session->ajax_members_filters)) {
-            $filters = $this->session->ajax_members_filters;
-        } else {
-            $filters = new MembersList();
-        }
+        $filters = $this->session->{$this->getFilterName(['prefix' => 'ajax'])} ?? new MembersList();
 
-        if (isset($args['option']) && $args['option'] == 'page') {
-            $filters->current_page = (int)$args['value'];
+        if ($option == 'page') {
+            $filters->current_page = (int)$value;
         }
 
         //numbers of rows to display
         if (isset($post['nbshow']) && is_numeric($post['nbshow'])) {
-            $filters->show = $post['nbshow'];
+            $filters->show = (int)$post['nbshow'];
         }
 
         $members = new Members($filters);
@@ -873,9 +768,9 @@ class MembersController extends CrudController
         }
 
         //assign pagination variables to the template and add pagination links
-        $filters->setSmartyPagination($this->router, $this->view->getSmarty(), false);
+        $filters->setViewPagination($this->routeparser, $this->view, false);
 
-        $this->session->ajax_members_filters = $filters;
+        $this->session->{$this->getFilterName(['prefix' => 'ajax'])} = $filters;
 
         $selected_members = null;
         $unreachables_members = null;
@@ -900,7 +795,6 @@ class MembersController extends CrudController
                             Analog::ERROR
                         );
                         throw new \Exception('A group id is required.');
-                        exit(0);
                     }
                     if (!isset($post['members'])) {
                         $group = new Group((int)$post['gid']);
@@ -915,7 +809,6 @@ class MembersController extends CrudController
                                 Analog::ERROR
                             );
                             throw new \Exception('Unknown mode.');
-                            exit(0);
                         }
                     } else {
                         $m = new Members();
@@ -930,7 +823,6 @@ class MembersController extends CrudController
                         throw new \RuntimeException(
                             'Current selected member must be excluded while attaching!'
                         );
-                        exit(0);
                     }
                     break;
             }
@@ -948,17 +840,17 @@ class MembersController extends CrudController
         }
 
         if (isset($post['gid'])) {
-            $params['the_id'] = $post['gid'];
+            $params['the_id'] = (int)$post['gid'];
         }
 
         if (isset($post['id_adh'])) {
-            $params['excluded'] = $post['id_adh'];
+            $params['excluded'] = (int)$post['id_adh'];
         }
 
         // display page
         $this->view->render(
             $response,
-            'ajax_members.tpl',
+            'elements/ajax_members.html.twig',
             $params
         );
         return $response;
@@ -972,60 +864,40 @@ class MembersController extends CrudController
      *
      * @return Response
      */
-    public function handleBatch(Request $request, Response $response) :Response
+    public function handleBatch(Request $request, Response $response)Response
     {
         $post = $request->getParsedBody();
 
-        if (isset($post['member_sel'])) {
-            if (isset($this->session->filter_members)) {
-                $filters = $this->session->filter_members;
+        if (isset($post['entries_sel'])) {
+            if (isset($this->session->{$this->getFilterName()})) {
+                $filters = $this->session->{$this->getFilterName()};
             } else {
                 $filters = new MembersList();
             }
 
-            $filters->selected = $post['member_sel'];
-            $this->session->filter_members = $filters;
-
-            if (isset($post['cards'])) {
-                return $response
-                    ->withStatus(301)
-                    ->withHeader('Location', $this->router->pathFor('pdf-members-cards'));
-            }
-
-            if (isset($post['labels'])) {
-                return $response
-                    ->withStatus(301)
-                    ->withHeader('Location', $this->router->pathFor('pdf-members-labels'));
-            }
-
-            if (isset($post['mailing'])) {
-                return $response
-                    ->withStatus(301)
-                    ->withHeader('Location', $this->router->pathFor('mailing') . '?mailing_new=new');
-            }
-
-            if (isset($post['attendance_sheet'])) {
-                return $response
-                    ->withStatus(301)
-                    ->withHeader('Location', $this->router->pathFor('attendance_sheet_details'));
-            }
-
-            if (isset($post['csv'])) {
-                return $response
-                    ->withStatus(301)
-                    ->withHeader('Location', $this->router->pathFor('csv-memberslist'));
-            }
-
-            if (isset($post['delete'])) {
-                return $response
-                    ->withStatus(301)
-                    ->withHeader('Location', $this->router->pathFor('removeMembers'));
-            }
+            $filters->selected = $post['entries_sel'];
+            $knowns = [
+                'cards' => 'pdf-members-cards',
+                'labels' => 'pdf-members-labels',
+                'sendmail' => 'mailing',
+                'attendance_sheet' => 'attendance_sheet_details',
+                'csv' => 'csv-memberslist',
+                'delete' => 'removeMembers',
+                'masschange' => 'masschangeMembers',
+                'masscontributions' => 'massAddContributionsChooseType'
+            ];
 
-            if (isset($post['masschange'])) {
-                return $response
-                    ->withStatus(301)
-                    ->withHeader('Location', $this->router->pathFor('masschangeMembers'));
+            foreach ($knowns as $known => $redirect_url) {
+                if (isset($post[$known])) {
+                    $this->session->{$this->getFilterName(['suffix' => $known])} = $filters;
+                    $redirect_url = $this->routeparser->urlFor($redirect_url);
+                    if ($known === 'sendmail') {
+                        $redirect_url .= '?mailing_new=new';
+                    }
+                    return $response
+                        ->withStatus(301)
+                        ->withHeader('Location', $redirect_url);
+                }
             }
 
             throw new \RuntimeException('Does not know what to batch :(');
@@ -1037,7 +909,7 @@ class MembersController extends CrudController
 
             return $response
                 ->withStatus(301)
-                ->withHeader('Location', $this->router->pathFor('members'));
+                ->withHeader('Location', $this->routeparser->urlFor('members'));
         }
     }
 
@@ -1049,69 +921,59 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param ?integer $id       Member id/array of members id
+     * @param string   $action   null or 'add'
      *
      * @return Response
      */
-    public function edit(Request $request, Response $response, array $args = []) :Response
-    {
-        $action = $args['action'];
-        $id = null;
-        if (isset($args['id'])) {
-            $id = (int)$args['id'];
-        }
-
-        if ($action === 'edit' && $id === null) {
-            throw new \RuntimeException(
-                _T("Member ID cannot ben null calling edit route!")
-            );
-        } elseif ($action === 'add' && $id !== null) {
-             return $response
-                ->withStatus(301)
-                ->withHeader('Location', $this->router->pathFor('editmember', ['action' => 'add']));
-        }
-        $deps = array(
-            'picture'   => true,
-            'groups'    => true,
-            'dues'      => true,
-            'parent'    => true,
-            'children'  => true,
-            'dynamics'  => true
-        );
+    public function edit(
+        Request $request,
+        Response $response,
+        int $id = null,
+        string $action = 'edit'
+    ): Response {
+        //instantiate member object
+        $member = new Adherent($this->zdb);
 
         if ($this->session->member !== null) {
+            //retrieve from session, in add or edit
             $member = $this->session->member;
             $this->session->member = null;
-        } else {
-            $member = new Adherent($this->zdb, $id, $deps);
+            $id = $member->id;
         }
+        $member->enableAllDeps();
 
         if ($id !== null) {
+            //load requested member
             $member->load($id);
-            if (!$member->canEdit($this->login)) {
-                $this->flash->addMessage(
-                    'error_detected',
-                    _T("You do not have permission for requested URL.")
+            $can = $member->canEdit($this->login);
+        } else {
+            $can = $member->canCreate($this->login);
+        }
+
+        if (!$can) {
+            $this->flash->addMessage(
+                'error_detected',
+                _T("You do not have permission for requested URL.")
+            );
+
+            return $response
+                ->withHeader(
+                    'Location',
+                    $this->routeparser->urlFor('me')
                 );
+        }
 
-                return $response
-                    ->withStatus(403)
-                    ->withHeader(
-                        'Location',
-                        $this->router->pathFor('me')
-                    );
-            }
-        } else {
-            if ($member->id != $id) {
-                $member->load($this->login->id);
-            }
+        //if adding a child, force parent here
+        if ($action === 'addchild') {
+            $member->setParent((int)$this->login->id);
         }
 
         // flagging required fields
         $fc = $this->fields_config;
 
         // password required if we create a new member
-        if ($member->id != '') {
+        if ($id !== null) {
             $fc->setNotRequired('mdp_adh');
         }
 
@@ -1143,6 +1005,8 @@ class MembersController extends CrudController
 
         //Status
         $statuts = new Status($this->zdb);
+        //Titles
+        $titles = new Titles($this->zdb);
 
         //Groups
         $groups = new Groups($this->zdb, $this->login);
@@ -1150,18 +1014,19 @@ class MembersController extends CrudController
 
         $form_elements = $fc->getFormElements(
             $this->login,
-            $member->id == ''
+            $id === null
         );
 
         // members
         $m = new Members();
-        $id = null;
+        $pid = null;
         if ($member->hasParent()) {
-            $id = ($member->parent instanceof Adherent ? $member->parent->id : $member->parent);
+            $pid = ($member->parent instanceof Adherent ? $member->parent->id : $member->parent);
         }
-        $members = $m->getSelectizedMembers(
+        $members = $m->getDropdownMembers(
             $this->zdb,
-            $id
+            $this->login,
+            $pid
         );
 
         $route_params['members'] = [
@@ -1173,25 +1038,31 @@ class MembersController extends CrudController
             $route_params['members']['list'] = $members;
         }
 
+        if ($action === 'edit') {
+            $route_params['navigate'] = $this->handleNavigationLinks($member->id);
+        }
+
         // display page
         $this->view->render(
             $response,
-            'member.tpl',
+            'pages/member_form.html.twig',
             array(
-                'parent_tpl'        => 'page.tpl',
+                'parent_tpl'        => 'page.html.twig',
                 'autocomplete'      => true,
                 'page_title'        => $title,
                 'member'            => $member,
                 'self_adh'          => false,
                 // pseudo random int
                 'time'              => time(),
-                'titles_list'       => Titles::getList($this->zdb),
+                'titles_list'       => $titles->getList(),
                 'statuts'           => $statuts->getList(),
                 'groups'            => $groups_list,
                 'fieldsets'         => $form_elements['fieldsets'],
                 'hidden_elements'   => $form_elements['hiddens'],
-                'parent_fields'     => $tpl_parent_fields
-            )
+                'parent_fields'     => $tpl_parent_fields,
+                'addchild'          => ($action === 'addchild'),
+                'osocials'          => new Social($this->zdb)
+            ) + $route_params
         );
         return $response;
     }
@@ -1201,13 +1072,13 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param integer  $id       Member id
      *
      * @return Response
      */
-    public function doEdit(Request $request, Response $response, array $args = []) :Response
+    public function doEdit(Request $request, Response $response, int $id): Response
     {
-        return $this->store($request, $response, $args);
+        return $this->store($request, $response);
     }
 
     /**
@@ -1215,53 +1086,47 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
      *
      * @return Response
      */
-    public function massChange(Request $request, Response $response, array $args = []) :Response
+    public function massChange(Request $request, Response $response): Response
     {
-        $filters =  $this->session->filter_members;
+        $filters = $this->session->{$this->getFilterName(['suffix' => 'masschange'])} ?? new MembersList();
 
         $data = [
             'id'            => $filters->selected,
-            'redirect_uri'  => $this->router->pathFor('members')
+            'redirect_uri'  => $this->routeparser->urlFor('members')
         ];
 
         $fc = $this->fields_config;
         $form_elements = $fc->getMassiveFormElements($this->members_fields, $this->login);
 
         //dynamic fields
-        $deps = array(
-            'picture'   => false,
-            'groups'    => false,
-            'dues'      => false,
-            'parent'    => false,
-            'children'  => false,
-            'dynamics'  => false
-        );
-        $member = new Adherent($this->zdb, null, $deps);
+        $member = new Adherent($this->zdb);
+        $member->disableAllDeps()->enableDep('dynamics');
 
         //Status
         $statuts = new Status($this->zdb);
+        //Titles
+        $titles = new Titles($this->zdb);
 
         // display page
         $this->view->render(
             $response,
-            'mass_change_members.tpl',
+            'modals/mass_change_members.html.twig',
             array(
-                'mode'          => $request->isXhr() ? 'ajax' : '',
+                'mode'          => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ? 'ajax' : '',
                 'page_title'    => str_replace(
                     '%count',
                     count($data['id']),
                     _T('Mass change %count members')
                 ),
-                'form_url'      => $this->router->pathFor('masschangeMembersReview'),
-                'cancel_uri'    => $this->router->pathFor('members'),
+                'form_url'      => $this->routeparser->urlFor('masschangeMembersReview'),
+                'cancel_uri'    => $this->routeparser->urlFor('members'),
                 'data'          => $data,
                 'member'        => $member,
                 'fieldsets'     => $form_elements['fieldsets'],
-                'titles_list'   => Titles::getList($this->zdb),
+                'titles_list'   => $titles->getList(),
                 'statuts'       => $statuts->getList(),
                 'require_mass'  => true
             )
@@ -1274,13 +1139,13 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
      *
      * @return Response
      */
-    public function validateMassChange(Request $request, Response $response, array $args = []) :Response
+    public function validateMassChange(Request $request, Response $response): Response
     {
         $post = $request->getParsedBody();
+        $changes = [];
 
         if (!isset($post['confirm'])) {
             $this->flash->addMessage(
@@ -1292,43 +1157,71 @@ class MembersController extends CrudController
             $fc = $this->fields_config;
             $form_elements = $fc->getMassiveFormElements($this->members_fields, $this->login);
 
-            $changes = [];
             foreach ($form_elements['fieldsets'] as $form_element) {
                 foreach ($form_element->elements as $field) {
-                    if (isset($post[$field->field_id]) && isset($post['mass_' . $field->field_id])) {
+                    if (
+                        isset($post['mass_' . $field->field_id])
+                        && (isset($post[$field->field_id]) || $field->type === FieldsConfig::TYPE_BOOL)
+                    ) {
                         $changes[$field->field_id] = [
                             'label' => $field->label,
-                            'value' => $post[$field->field_id]
+                            'value' => $post[$field->field_id] ?? 0
                         ];
                     }
                 }
             }
+
+            //handle dynamic fields
+            $member = new Adherent($this->zdb);
+            $member
+                ->enableAllDeps()
+                ->setDependencies(
+                    $this->preferences,
+                    $this->members_fields,
+                    $this->history
+                );
+            $dynamic_fields = $member->getDynamicFields()->getFields();
+            foreach ($dynamic_fields as $field) {
+                $mass_id = 'mass_info_field_' . $field->getId();
+                $field_id = 'info_field_' . $field->getId() . '_1';
+                if (
+                    isset($post[$mass_id])
+                    && (isset($post[$field_id]) || $field instanceof Boolean)
+                ) {
+                    $changes[$field_id] = [
+                        'label' => $field->getName(),
+                        'value' => $post[$field_id] ?? 0
+                    ];
+                }
+            }
         }
 
-        $filters =  $this->session->filter_members;
+        $filters = $this->session->{$this->getFilterName(['suffix' => 'masschange'])};
         $data = [
             'id'            => $filters->selected,
-            'redirect_uri'  => $this->router->pathFor('members')
+            'redirect_uri'  => $this->routeparser->urlFor('members')
         ];
 
         //Status
         $statuts = new Status($this->zdb);
+        //Titles
+        $titles = new Titles($this->zdb);
 
         // display page
         $this->view->render(
             $response,
-            'mass_change_members.tpl',
+            'modals/mass_change_members.html.twig',
             array(
-                'mode'          => $request->isXhr() ? 'ajax' : '',
+                'mode'          => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ? 'ajax' : '',
                 'page_title'    => str_replace(
                     '%count',
                     count($data['id']),
                     _T('Review mass change %count members')
                 ),
-                'form_url'      => $this->router->pathFor('massstoremembers'),
-                'cancel_uri'    => $this->router->pathFor('members'),
+                'form_url'      => $this->routeparser->urlFor('massstoremembers'),
+                'cancel_uri'    => $this->routeparser->urlFor('members'),
                 'data'          => $data,
-                'titles_list'   => Titles::getList($this->zdb),
+                'titles_list'   => $titles->getList(),
                 'statuts'       => $statuts->getList(),
                 'changes'       => $changes
             )
@@ -1341,16 +1234,16 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
      *
      * @return Response
      */
-    public function doMassChange(Request $request, Response $response, array $args = []) :Response
+    public function doMassChange(Request $request, Response $response): Response
     {
         $post = $request->getParsedBody();
         $redirect_url = $post['redirect_uri'];
         $error_detected = [];
         $mass = 0;
+        $dynamic_fields = null;
 
         unset($post['redirect_uri']);
         if (!isset($post['confirm'])) {
@@ -1368,9 +1261,33 @@ class MembersController extends CrudController
                 foreach ($form_elements['fieldsets'] as $fieldset) {
                     if (isset($fieldset->elements[$key])) {
                         $found = true;
-                        continue;
+                        break;
+                    }
+                }
+
+                if (!$found) {
+                    //try on dynamic fields
+                    if ($dynamic_fields === null) {
+                        //handle dynamic fields
+                        $member = new Adherent($this->zdb);
+                        $member
+                            ->enableAllDeps()
+                            ->setDependencies(
+                                $this->preferences,
+                                $this->members_fields,
+                                $this->history
+                            );
+                        $dynamic_fields = $member->getDynamicFields()->getFields();
+                    }
+                    foreach ($dynamic_fields as $field) {
+                        $field_id = 'info_field_' . $field->getId() . '_1';
+                        if ($key == $field_id) {
+                            $found = true;
+                            break;
+                        }
                     }
                 }
+
                 if (!$found) {
                     Analog::log(
                         'Permission issue mass editing field ' . $key,
@@ -1389,15 +1306,14 @@ class MembersController extends CrudController
                     $is_manager = !$this->login->isAdmin()
                         && !$this->login->isStaff()
                         && $this->login->isGroupManager();
-                    $deps = array(
-                        'picture'   => false,
-                        'groups'    => $is_manager,
-                        'dues'      => false,
-                        'parent'    => false,
-                        'children'  => false,
-                        'dynamics'  => false
-                    );
-                    $member = new Adherent($this->zdb, (int)$id, $deps);
+                    $member = new Adherent($this->zdb);
+                    $member
+                        ->disableAllDeps()
+                        ->disableEvents();
+                    if ($is_manager) {
+                        $member->enableDep('groups');
+                    }
+                    $member->load((int)$id);
                     $member->setDependencies(
                         $this->preferences,
                         $this->members_fields,
@@ -1444,14 +1360,15 @@ class MembersController extends CrudController
             }
         }
 
-        if (!$request->isXhr()) {
+        if (!($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')) {
             return $response
                 ->withStatus(301)
                 ->withHeader('Location', $redirect_url);
         } else {
-            return $response->withJson(
+            return $this->withJson(
+                $response,
                 [
-                    'success'   => count($error_detected) === 0
+                    'success' => count($error_detected) === 0
                 ]
             );
         }
@@ -1462,72 +1379,70 @@ class MembersController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
      *
      * @return Response
      */
-    public function store(Request $request, Response $response, array $args = []) :Response
+    public function store(Request $request, Response $response): Response
     {
         if (!$this->preferences->pref_bool_selfsubscribe && !$this->login->isLogged()) {
             return $response
                 ->withStatus(301)
-                ->withHeader('Location', $this->router->pathFor('slash'));
+                ->withHeader('Location', $this->routeparser->urlFor('slash'));
         }
 
         $post = $request->getParsedBody();
-        $deps = array(
-            'picture'   => true,
-            'groups'    => true,
-            'dues'      => true,
-            'parent'    => true,
-            'children'  => true,
-            'dynamics'  => true
-        );
-        $member = new Adherent($this->zdb, null, $deps);
-        $member->setDependencies(
-            $this->preferences,
-            $this->members_fields,
-            $this->history
-        );
-        if (isset($args['self'])) {
-            //mark as self membership
-            $member->setSelfMembership();
-        }
+        $member = new Adherent($this->zdb);
+        $member
+            ->enableAllDeps()
+            ->setDependencies(
+                $this->preferences,
+                $this->members_fields,
+                $this->history
+            );
 
         $success_detected = [];
-        $warning_detected = [];
         $error_detected = [];
 
+        if ($this->isSelfMembership() && !isset($post[Adherent::PK])) {
+            //mark as self membership
+            $member->setSelfMembership();
+
+            //check captcha
+            $gaptcha = $this->session->gaptcha;
+            if (!$gaptcha->check($post['gaptcha'])) {
+                $error_detected[] = _T('Invalid captcha');
+            }
+        }
+
         // new or edit
-        $adherent['id_adh'] = get_numeric_form_value('id_adh', '');
-        if ($this->login->isAdmin() || $this->login->isStaff() || $this->login->isGroupManager()) {
-            if ($adherent['id_adh']) {
-                $member->load((int)$adherent['id_adh']);
-                if (!$member->canEdit($this->login)) {
-                    //redirection should have been done before. Just throw an Exception.
-                    throw new \RuntimeException(
-                        str_replace(
-                            '%id',
-                            $member->id,
-                            'No right to store member #%id'
-                        )
-                    );
-                }
+        if (isset($post['id_adh'])) {
+            $member->load((int)$post['id_adh']);
+            if (!$member->canEdit($this->login)) {
+                //redirection should have been done before. Just throw an Exception.
+                throw new \RuntimeException(
+                    str_replace(
+                        '%id',
+                        $member->id,
+                        'No right to store member #%id'
+                    )
+                );
             }
         } else {
-            $member->load($this->login->id);
-            $adherent['id_adh'] = $this->login->id;
+            if ($member->id != '') {
+                $member->load($this->login->id);
+            }
         }
 
         // flagging required fields
         $fc = $this->fields_config;
 
-        // password required if we create a new member
-        if ($member->id != '') {
+        // password required if we create a new member but not from self subscription
+        if ($member->id != '' || $this->isSelfMembership()) {
             $fc->setNotRequired('mdp_adh');
         }
 
-        if ($member->hasParent() && !isset($post['detach_parent'])
+        if (
+            $member->hasParent() && !isset($post['detach_parent'])
             || isset($post['parent_id']) && !empty($post['parent_id'])
         ) {
             $parent_fields = $member->getParentFields();
@@ -1547,7 +1462,7 @@ class MembersController extends CrudController
         $form_elements = $fc->getFormElements(
             $this->login,
             $member->id == '',
-            isset($args['self'])
+            $this->isSelfMembership()
         );
         $fieldsets     = $form_elements['fieldsets'];
         $required      = array();
@@ -1575,9 +1490,14 @@ class MembersController extends CrudController
 
         $real_requireds = array_diff(array_keys($required), array_keys($disabled));
 
+        // send email to member
+        if ($this->isSelfMembership() || isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
+            $member->setSendmail(); //flag to send creation email
+        }
+
         // Validation
-        $redirect_url = $this->router->pathFor('member', ['id' => $member->id]);
-        if (isset($post[array_shift($real_requireds)])) {
+        $redirect_url = $this->routeparser->urlFor('member', ['id' => $member->id]);
+        if (!count($real_requireds) || isset($post[array_shift($real_requireds)])) {
             // regular fields
             $valid = $member->check($post, $required, $disabled);
             if ($valid !== true) {
@@ -1591,13 +1511,15 @@ class MembersController extends CrudController
                 if ($member->id == '') {
                     $new = true;
                 }
+
                 $store = $member->store();
                 if ($store === true) {
                     //member has been stored :)
                     if ($new) {
-                        if (isset($args['self'])) {
+                        if ($this->isSelfMembership()) {
                             $success_detected[] = _T("Your account has been created!");
-                            if ($this->preferences->pref_mail_method > GaletteMail::METHOD_DISABLED
+                            if (
+                                $this->preferences->pref_mail_method > GaletteMail::METHOD_DISABLED
                                 && $member->getEmail() != ''
                             ) {
                                 $success_detected[] = _T("An email has been sent to you, check your inbox.");
@@ -1605,272 +1527,50 @@ class MembersController extends CrudController
                         } else {
                             $success_detected[] = _T("New member has been successfully added.");
                         }
-                        //Send email to admin if preference checked
-                        if ($this->preferences->pref_mail_method > GaletteMail::METHOD_DISABLED
-                            && $this->preferences->pref_bool_mailadh
-                        ) {
-                            $texts = new Texts(
-                                $this->preferences,
-                                $this->router,
-                                array(
-                                    'name_adh'      => custom_html_entity_decode(
-                                        $member->sname
-                                    ),
-                                    'firstname_adh' => custom_html_entity_decode(
-                                        $member->surname
-                                    ),
-                                    'lastname_adh'  => custom_html_entity_decode(
-                                        $member->name
-                                    ),
-                                    'mail_adh'      => custom_html_entity_decode(
-                                        $member->email
-                                    ),
-                                    'login_adh'     => custom_html_entity_decode(
-                                        $member->login
-                                    )
-                                )
-                            );
-                            $mtxt = $texts->getTexts(
-                                (isset($args['self']) ? 'newselfadh' : 'newadh'),
-                                $this->preferences->pref_lang
-                            );
-
-                            $mail = new GaletteMail($this->preferences);
-                            $mail->setSubject($texts->getSubject());
-                            $recipients = [];
-                            foreach ($this->preferences->vpref_email_newadh as $pref_email) {
-                                $recipients[$pref_email] = $pref_email;
-                            }
-                            $mail->setRecipients($recipients);
-                            $mail->setMessage($texts->getBody());
-                            $sent = $mail->send();
-
-                            if ($sent == GaletteMail::MAIL_SENT) {
-                                $this->history->add(
-                                    str_replace(
-                                        '%s',
-                                        $member->sname . ' (' . $member->email . ')',
-                                        _T("New account email sent to admin for '%s'.")
-                                    )
-                                );
-                            } else {
-                                $str = str_replace(
-                                    '%s',
-                                    $member->sname . ' (' . $member->email . ')',
-                                    _T("A problem happened while sending email to admin for account '%s'.")
-                                );
-                                $this->history->add($str);
-                                $warning_detected[] = $str;
-                            }
-                            unset($texts);
-                        }
                     } else {
                         $success_detected[] = _T("Member account has been modified.");
                     }
 
-                    // send email to member
-                    if (isset($args['self']) || isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
-                        if ($this->preferences->pref_mail_method > GaletteMail::METHOD_DISABLED) {
-                            if ($member->getEmail() == '' && !isset($args['self'])) {
-                                $error_detected[] = _T("- You can't send a confirmation by email if the member hasn't got an address!");
-                            } else {
-                                $mreplaces = [
-                                    'name_adh'      => custom_html_entity_decode(
-                                        $member->sname
-                                    ),
-                                    'firstname_adh' => custom_html_entity_decode(
-                                        $member->surname
-                                    ),
-                                    'lastname_adh'  => custom_html_entity_decode(
-                                        $member->name
-                                    ),
-                                    'mail_adh'      => custom_html_entity_decode(
-                                        $member->getEmail()
-                                    ),
-                                    'login_adh'     => custom_html_entity_decode(
-                                        $member->login
-                                    )
-                                ];
-                                if ($new) {
-                                    $password = new Password($this->zdb);
-                                    $res = $password->generateNewPassword($member->id);
-                                    if ($res == true) {
-                                        $link_validity = new DateTime();
-                                        $link_validity->add(new DateInterval('PT24H'));
-                                        $mreplaces['change_pass_uri'] = $this->preferences->getURL() .
-                                            $this->router->pathFor(
-                                                'password-recovery',
-                                                ['hash' => base64_encode($password->getHash())]
-                                            );
-                                        $mreplaces['link_validity'] = $link_validity->format(_T("Y-m-d H:i:s"));
-                                    } else {
-                                        $str = str_replace(
-                                            '%s',
-                                            $login_adh,
-                                            _T("An error occurred storing temporary password for %s. Please inform an admin.")
-                                        );
-                                        $this->history->add($str);
-                                        $this->flash->addMessage(
-                                            'error_detected',
-                                            $str
-                                        );
-                                    }
-                                }
-
-                                //send email to member
-                                // Get email text in database
-                                $texts = new Texts(
-                                    $this->preferences,
-                                    $this->router,
-                                    $mreplaces
-                                );
-                                $mlang = $this->preferences->pref_lang;
-                                if (isset($post['pref_lang'])) {
-                                    $mlang = $post['pref_lang'];
-                                }
-                                $mtxt = $texts->getTexts(
-                                    (($new) ? 'sub' : 'accountedited'),
-                                    $mlang
-                                );
+                    if ($this->login->isGroupManager()) {
+                        //add/remove user from groups
+                        $groups_adh = $post['groups_adh'] ?? null;
+                        $add_groups = Groups::addMemberToGroups(
+                            $member,
+                            $groups_adh
+                        );
 
-                                $mail = new GaletteMail($this->preferences);
-                                $mail->setSubject($texts->getSubject());
-                                $mail->setRecipients(
-                                    array(
-                                        $member->getEmail() => $member->sname
-                                    )
-                                );
-                                $mail->setMessage($texts->getBody());
-                                $sent = $mail->send();
-
-                                if ($sent == GaletteMail::MAIL_SENT) {
-                                    $msg = str_replace(
-                                        '%s',
-                                        $member->sname . ' (' . $member->getEmail() . ')',
-                                        ($new) ?
-                                        _T("New account email sent to '%s'.") :
-                                        _T("Account modification email sent to '%s'.")
-                                    );
-                                    $this->history->add($msg);
-                                    $success_detected[] = $msg;
-                                } else {
-                                    $str = str_replace(
-                                        '%s',
-                                        $member->sname . ' (' . $member->getEmail() . ')',
-                                        _T("A problem happened while sending account email to '%s'")
-                                    );
-                                    $this->history->add($str);
-                                    $error_detected[] = $str;
-                                }
-                            }
-                        } elseif ($this->preferences->pref_mail_method == GaletteMail::METHOD_DISABLED) {
-                            //if email has been disabled in the preferences, we should not be here ;
-                            //we do not throw an error, just a simple warning that will be show later
-                            $msg = _T("You asked Galette to send a confirmation email to the member, but email has been disabled in the preferences.");
-                            $warning_detected[] = $msg;
+                        if ($add_groups === false) {
+                            $error_detected[] = _T("An error occurred adding member to its groups.");
                         }
                     }
-
-                    // send email to admin
-                    if ($this->preferences->pref_mail_method > GaletteMail::METHOD_DISABLED
-                        && $this->preferences->pref_bool_mailadh
-                        && !$new
-                        && $member->id == $this->login->id
-                    ) {
-                        $mreplaces = [
-                            'name_adh'      => custom_html_entity_decode(
-                                $member->sname
-                            ),
-                            'firstname_adh' => custom_html_entity_decode(
-                                $member->surname
-                            ),
-                            'lastname_adh'  => custom_html_entity_decode(
-                                $member->name
-                            ),
-                            'mail_adh'      => custom_html_entity_decode(
-                                $member->getEmail()
-                            ),
-                            'login_adh'     => custom_html_entity_decode(
-                                $member->login
-                            )
-                        ];
-
-                        //send email to member
-                        // Get email text in database
-                        $texts = new Texts(
-                            $this->preferences,
-                            $this->router,
-                            $mreplaces
-                        );
-                        $mlang = $this->preferences->pref_lang;
-
-                        $mtxt = $texts->getTexts(
-                            'admaccountedited',
-                            $mlang
+                    if ($this->login->isSuperAdmin() || $this->login->isAdmin() || $this->login->isStaff()) {
+                        //add/remove manager from groups
+                        $managed_groups_adh = $post['groups_managed_adh'] ?? null;
+                        $add_groups = Groups::addMemberToGroups(
+                            $member,
+                            $managed_groups_adh,
+                            true
                         );
+                        $member->loadGroups();
 
-                        $mail = new GaletteMail($this->preferences);
-                        $mail->setSubject($texts->getSubject());
-                        $recipients = [];
-                        foreach ($this->preferences->vpref_email_newadh as $pref_email) {
-                            $recipients[$pref_email] = $pref_email;
-                        }
-                        $mail->setRecipients($recipients);
-
-                        $mail->setMessage($texts->getBody());
-                        $sent = $mail->send();
-
-                        if ($sent == GaletteMail::MAIL_SENT) {
-                            $msg = _T("Account modification email sent to admin.");
-                            $this->history->add($msg);
-                            $success_detected[] = $msg;
-                        } else {
-                            $str = _T("A problem happened while sending account email to admin");
-                            $this->history->add($str);
-                            $warning_detected[] = $str;
+                        if ($add_groups === false) {
+                            $error_detected[] = _T("An error occurred adding member to its groups as manager.");
                         }
                     }
-
-                    //store requested groups
-                    $add_groups = null;
-                    $groups_adh = null;
-                    $managed_groups_adh = null;
-
-                    //add/remove user from groups
-                    if (isset($post['groups_adh'])) {
-                        $groups_adh = $post['groups_adh'];
-                    }
-                    $add_groups = Groups::addMemberToGroups(
-                        $member,
-                        $groups_adh
-                    );
-
-                    if ($add_groups === false) {
-                        $error_detected[] = _T("An error occurred adding member to its groups.");
-                    }
-
-                    //add/remove manager from groups
-                    if (isset($post['groups_managed_adh'])) {
-                        $managed_groups_adh = $post['groups_managed_adh'];
-                    }
-                    $add_groups = Groups::addMemberToGroups(
-                        $member,
-                        $managed_groups_adh,
-                        true
-                    );
-                    $member->loadGroups();
-
-                    if ($add_groups === false) {
-                        $error_detected[] = _T("An error occurred adding member to its groups as manager.");
-                    }
                 } else {
                     //something went wrong :'(
                     $error_detected[] = _T("An error occurred while storing the member.");
                 }
             }
 
-            if (count($error_detected) == 0) {
-                $files_res = $member->handleFiles($_FILES);
+            if (count($error_detected) === 0) {
+                $cropping = null;
+                if ($this->preferences->pref_force_picture_ratio == 1) {
+                    $cropping = [];
+                    $cropping['ratio'] = isset($this->preferences->pref_member_picture_ratio) ? $this->preferences->pref_member_picture_ratio : 'square_ratio';
+                    $cropping['focus'] = isset($post['crop_focus']) ? $post['crop_focus'] : 'center';
+                }
+                $files_res = $member->handleFiles($_FILES, $cropping);
                 if (is_array($files_res)) {
                     $error_detected = array_merge($error_detected, $files_res);
                 }
@@ -1878,7 +1578,7 @@ class MembersController extends CrudController
                 if (isset($post['del_photo'])) {
                     if (!$member->picture->delete($member->id)) {
                         $error_detected[] = _T("Delete failed");
-                        $str_adh = $member->id . ' (' . $member->sname  . ' ' . ')';
+                        $str_adh = $member->id . ' (' . $member->sname . ' ' . ')';
                         Analog::log(
                             'Unable to delete picture for member ' . $str_adh,
                             Analog::ERROR
@@ -1891,7 +1591,7 @@ class MembersController extends CrudController
                 foreach ($error_detected as $error) {
                     if (strpos($error, '%member_url_') !== false) {
                         preg_match('/%member_url_(\d+)/', $error, $matches);
-                        $url = $this->router->pathFor('member', ['id' => $matches[1]]);
+                        $url = $this->routeparser->urlFor('member', ['id' => $matches[1]]);
                         $error = str_replace(
                             '%member_url_' . $matches[1],
                             $url,
@@ -1905,14 +1605,6 @@ class MembersController extends CrudController
                 }
             }
 
-            if (count($warning_detected) > 0) {
-                foreach ($warning_detected as $warning) {
-                    $this->flash->addMessage(
-                        'warning_detected',
-                        $warning
-                    );
-                }
-            }
             if (count($success_detected) > 0) {
                 foreach ($success_detected as $success) {
                     $this->flash->addMessage(
@@ -1922,59 +1614,53 @@ class MembersController extends CrudController
                 }
             }
 
-            if (count($error_detected) == 0) {
-                if (isset($args['self'])) {
-                    $redirect_url = $this->router->pathFor('login');
-                } elseif (isset($post['redirect_on_create'])
+            if (count($error_detected) === 0) {
+                if ($this->isSelfMembership()) {
+                    $redirect_url = $this->routeparser->urlFor('login');
+                } elseif (
+                    isset($post['redirect_on_create'])
                     && $post['redirect_on_create'] > Adherent::AFTER_ADD_DEFAULT
                 ) {
                     switch ($post['redirect_on_create']) {
                         case Adherent::AFTER_ADD_TRANS:
-                            $redirect_url = $this->router->pathFor('transaction', ['action' => 'add']);
+                            $redirect_url = $this->routeparser->urlFor('addTransaction');
                             break;
                         case Adherent::AFTER_ADD_NEW:
-                            $redirect_url = $this->router->pathFor('editmember', ['action' => 'add']);
+                            $redirect_url = $this->routeparser->urlFor('addMember');
                             break;
                         case Adherent::AFTER_ADD_SHOW:
-                            $redirect_url = $this->router->pathFor('member', ['id' => $member->id]);
+                            $redirect_url = $this->routeparser->urlFor('member', ['id' => $member->id]);
                             break;
                         case Adherent::AFTER_ADD_LIST:
-                            $redirect_url = $this->router->pathFor('members');
+                            $redirect_url = $this->routeparser->urlFor('members');
                             break;
                         case Adherent::AFTER_ADD_HOME:
-                            $redirect_url = $this->router->pathFor('slash');
+                            $redirect_url = $this->routeparser->urlFor('slash');
                             break;
                     }
                 } elseif (!isset($post['id_adh']) && !$member->isDueFree()) {
-                    $redirect_url = $this->router->pathFor(
-                        'contribution',
-                        [
-                            'type'      => 'fee',
-                            'action'    => 'add',
-                        ]
+                    $redirect_url = $this->routeparser->urlFor(
+                        'addContribution',
+                        ['type' => 'fee']
                     ) . '?id_adh=' . $member->id;
                 } else {
-                    $redirect_url = $this->router->pathFor('member', ['id' => $member->id]);
+                    $redirect_url = $this->routeparser->urlFor('member', ['id' => $member->id]);
                 }
             } else {
                 //store entity in session
                 $this->session->member = $member;
 
-                if (isset($args['self'])) {
-                    $redirect_url = $this->router->pathFor('subscribe');
+                if ($this->isSelfMembership()) {
+                    $redirect_url = $this->routeparser->urlFor('subscribe');
                 } else {
                     if ($member->id) {
-                        $rparams = [
-                            'id'    => $member->id,
-                            'action'    => 'edit'
-                        ];
+                        $redirect_url = $this->routeparser->urlFor(
+                            'editMember',
+                            ['id'    => $member->id]
+                        );
                     } else {
-                        $rparams = ['action' => 'add'];
+                        $redirect_url = $this->routeparser->urlFor((isset($post['addchild']) ? 'addMemberChild' : 'addMember'));
                     }
-                    $redirect_url = $this->router->pathFor(
-                        'editmember',
-                        $rparams
-                    );
                 }
             }
         }
@@ -1995,9 +1681,9 @@ class MembersController extends CrudController
      *
      * @return string
      */
-    public function redirectUri(array $args = [])
+    public function redirectUri(array $args)
     {
-        return $this->router->pathFor('members');
+        return $this->routeparser->urlFor('members');
     }
 
     /**
@@ -2007,37 +1693,14 @@ class MembersController extends CrudController
      *
      * @return string
      */
-    public function formUri(array $args = [])
+    public function formUri(array $args)
     {
-        return $this->router->pathFor(
+        return $this->routeparser->urlFor(
             'doRemoveMember',
             $args
         );
     }
 
-
-    /**
-     * Get ID to remove
-     *
-     * In simple cases, we get the ID in the route arguments; but for
-     * batchs, it should be found elsewhere.
-     * In post values, we look for id key, as well as all {sthing}_sel keys (like members_sel or contrib_sel)
-     *
-     * @param array $args Request arguments
-     * @param array $post POST values
-     *
-     * @return null|integer|integer[]
-     */
-    protected function getIdsToRemove(&$args, $post)
-    {
-        if (isset($args['id'])) {
-            return $args['id'];
-        } else {
-            $filters =  $this->session->filter_members;
-            return $filters->selected;
-        }
-    }
-
     /**
      * Get confirmation removal page title
      *
@@ -2045,18 +1708,20 @@ class MembersController extends CrudController
      *
      * @return string
      */
-    public function confirmRemoveTitle(array $args = [])
+    public function confirmRemoveTitle(array $args)
     {
-        if (isset($args['id_adh'])) {
+        if (isset($args['id_adh']) || isset($args['id'])) {
             //one member removal
-            $adh = new Adherent($this->zdb, (int)$args['id_adh']);
+            $id_adh = $args['id_adh'] ?? $args['id'];
+            $adh = new Adherent($this->zdb, (int)$id_adh);
             return sprintf(
                 _T('Remove member %1$s'),
                 $adh->sfullname
             );
         } else {
             //batch members removal
-            $filters =  $this->session->filter_members;
+            $filters = $this->session->{$this->getFilterName(['suffix' => 'delete'])};
+            $this->session->{$this->getFilterName(['suffix' => 'delete'])} = $filters;
             return str_replace(
                 '%count',
                 count($filters->selected),
@@ -2071,12 +1736,12 @@ class MembersController extends CrudController
      * @param array $args Route arguments
      * @param array $post POST values
      *
-     * @return boolean
+     * @return bool
      */
     protected function doDelete(array $args, array $post)
     {
-        if (isset($this->session->filter_members)) {
-            $filters =  $this->session->filter_members;
+        if (isset($this->session->{$this->getFilterName(['suffix' => 'delete'])})) {
+            $filters = $this->session->{$this->getFilterName(['suffix' => 'delete'])};
         } else {
             $filters = new MembersList();
         }
@@ -2092,4 +1757,103 @@ class MembersController extends CrudController
     }
 
     // CRUD - Delete
+
+    /**
+     * Set self memebrship flag
+     *
+     * @return MembersController
+     */
+    private function setSelfMembership(): MembersController
+    {
+        $this->is_self_membership = true;
+        return $this;
+    }
+
+    /**
+     * Is self membership?
+     *
+     * @return bool
+     */
+    private function isSelfMembership(): bool
+    {
+        return $this->is_self_membership;
+    }
+
+    /**
+     * Handle navigation links
+     *
+     * @param int $id_adh Current member ID
+     *
+     * @return array
+     */
+    private function handleNavigationLinks(int $id_adh): array
+    {
+        $navigate = array();
+
+        if (isset($this->session->{$this->getFilterName()})) {
+            $filters = clone $this->session->{$this->getFilterName()};
+        } else {
+            $filters = new MembersList();
+        }
+        //we must navigate between all members
+        $filters->show = 0;
+
+        if (
+            $this->login->isAdmin()
+            || $this->login->isStaff()
+            || $this->login->isGroupManager()
+        ) {
+            $m = new Members($filters);
+
+            $ids = array();
+            $fields = [Adherent::PK, 'nom_adh', 'prenom_adh'];
+            if ($this->login->isAdmin() || $this->login->isStaff()) {
+                $ids = $m->getMembersList(false, $fields);
+            } else {
+                $ids = $m->getManagedMembersList(false, $fields);
+            }
+
+            $ids = $ids->toArray();
+            foreach ($ids as $k => $m) {
+                if ($m['id_adh'] == $id_adh) {
+                    $navigate = array(
+                        'cur'  => $m['id_adh'],
+                        'count' => $filters->counter,
+                        'pos' => $k + 1
+                    );
+                    if ($k > 0) {
+                        $navigate['prev'] = $ids[$k - 1]['id_adh'];
+                    }
+                    if ($k < count($ids) - 1) {
+                        $navigate['next'] = $ids[$k + 1]['id_adh'];
+                    }
+                    break;
+                }
+            }
+        }
+
+        return $navigate;
+    }
+
+    /**
+     * Get filter name in session
+     *
+     * @param array|null $args Route arguments
+     *
+     * @return string
+     */
+    public function getFilterName(array $args = null): string
+    {
+        $filter_name = 'filter_members';
+
+        if (isset($args['prefix'])) {
+            $filter_name = $args['prefix'] . '_' . $filter_name;
+        }
+
+        if (isset($args['suffix'])) {
+            $filter_name .= '_' . $args['suffix'];
+        }
+
+        return $filter_name;
+    }
 }