. * * @category Controllers * @package Galette * * @author Johan Cwiklinski * @copyright 2019-2021 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 */ namespace Galette\Controllers\Crud; use Galette\Controllers\CrudController; use Galette\DynamicFields\Boolean; use Slim\Http\Request; use Slim\Http\Response; use Galette\Core\GaletteMail; use Galette\Core\Gaptcha; use Galette\Entity\Adherent; use Galette\Entity\Contribution; use Galette\Entity\ContributionsTypes; use Galette\Entity\DynamicFieldsHandle; use Galette\Entity\Group; use Galette\Entity\Status; use Galette\Entity\FieldsConfig; use Galette\Filters\AdvancedMembersList; use Galette\Filters\MembersList; use Galette\IO\File; use Galette\Repository\Groups; use Galette\Repository\Members; use Galette\Repository\PaymentTypes; use Galette\Repository\Titles; use Analog\Analog; /** * Galette members controller * * @category Controllers * @name GaletteController * @package Galette * @author Johan Cwiklinski * @copyright 2019-2021 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 */ class MembersController extends CrudController { /** @var bool */ private $is_self_membership = false; // CRUD - Create /** * Add page * * @param Request $request PSR Request * @param Response $response PSR Response * * @return 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, null, 'addchild'); } /** * Self subscription page * * @param Request $request PSR Request * @param Response $response PSR Response * * @return 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')); } if ($this->session->member !== null) { $member = $this->session->member; $this->session->member = null; } else { $member = new Adherent($this->zdb); $member->enableDep('dynamics'); } //mark as self membership $member->setSelfMembership(); // flagging required fields $fc = $this->fields_config; $form_elements = $fc->getFormElements($this->login, true, true); // members $m = new Members(); $members = $m->getSelectizedMembers( $this->zdb, $member->hasParent() ? $member->parent->id : null ); $params = [ 'members' => [ 'filters' => $m->getFilters(), 'count' => $m->getCount() ] ]; if (count($members)) { $params['members']['list'] = $members; } $gaptcha = new Gaptcha($this->i18n); $this->session->gaptcha = $gaptcha; // display page $this->view->render( $response, 'member.tpl', array( 'page_title' => _T("Subscription"), 'parent_tpl' => 'public_page.tpl', 'member' => $member, 'self_adh' => true, 'autocomplete' => true, // pseudo random int 'time' => time(), 'titles_list' => Titles::getList($this->zdb), 'fieldsets' => $form_elements['fieldsets'], 'hidden_elements' => $form_elements['hiddens'], //self_adh specific 'gaptcha' => $gaptcha ) + $params ); return $response; } /** * Add action * * @param Request $request PSR Request * @param Response $response PSR Response * * @return 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 { $this->setSelfMembership(); return $this->doAdd($request, $response); } /** * Duplicate action * * @param Request $request PSR Request * @param Response $response PSR Response * @param integer $id_adh Member ID to duplicate * * @return Response */ public function duplicate(Request $request, Response $response, int $id_adh): Response { $adh = new Adherent($this->zdb, $id_adh, ['dynamics' => true, 'parent' => true]); $adh->setDuplicate(); //store entity in session $this->session->member = $adh; return $response ->withStatus(301) ->withHeader('Location', $this->router->pathFor('addMember')); } // /CRUD - Create // CRUD - Read /** * Display member card * * @param Request $request PSR Request * @param Response $response PSR Response * @param integer $id Member ID * * @return Response */ public function show(Request $request, Response $response, int $id): Response { $member = new Adherent($this->zdb); $member ->enableAllDeps() ->load($id); if (!$member->canShow($this->login)) { $this->flash->addMessage( 'error_detected', _T("You do not have permission for requested URL.") ); return $response ->withStatus(301) ->withHeader( 'Location', $this->router->pathFor('me') ); } if ($member->id == null) { //member does not exists! $this->flash->addMessage( 'error_detected', str_replace('%id', $id, _T("No member #%id.")) ); return $response ->withHeader( 'Location', $this->router->pathFor('slash') ); } // flagging fields visibility $fc = $this->fields_config; $display_elements = $fc->getDisplayElements($this->login); // display page $this->view->render( $response, 'voir_adherent.tpl', array( 'page_title' => _T("Member Profile"), 'member' => $member, 'pref_lang' => $this->i18n->getNameFromId($member->language), 'pref_card_self' => $this->preferences->pref_card_self, 'groups' => Groups::getSimpleList(), 'time' => time(), 'display_elements' => $display_elements ) ); return $response; } /** * Own card show * * @param Request $request PSR Request * @param Response $response PSR Response * * @return Response */ public function showMe(Request $request, Response $response): Response { if ($this->login->isSuperAdmin()) { return $response ->withStatus(301) ->withHeader('Location', $this->router->pathFor('slash')); } return $this->show($request, $response, $this->login->id); } /** * Public pages (trombinoscope, public list) * * @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, $option = null, $value = null, $type = null ): Response { $varname = 'public_filter_' . $type; if (isset($this->session->$varname)) { $filters = $this->session->$varname; } else { $filters = new MembersList(); } if ($option !== null) { switch ($option) { case 'page': $filters->current_page = (int)$value; break; case 'order': $filters->orderby = $value; break; } } $m = new Members($filters); $members = $m->getPublicList($type === 'trombi'); $this->session->$varname = $filters; //assign pagination variables to the template and add pagination links $filters->setSmartyPagination($this->router, $this->view->getSmarty(), false); // display page $this->view->render( $response, ($type === 'list' ? 'liste_membres' : 'trombinoscope') . '.tpl', 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 ) ); return $response; } /** * Public pages (trombinoscope, public list) * * @param Request $request PSR Request * @param Response $response PSR Response * @param string $type Type * * @return Response */ public function filterPublicList(Request $request, Response $response, string $type): Response { $post = $request->getParsedBody(); $varname = 'public_filter_' . $type; if (isset($this->session->$varname)) { $filters = $this->session->$varname; } else { $filters = new MembersList(); } //reintialize filters if (isset($post['clear_filter'])) { $filters->reinit(); } else { //number of rows to show if (isset($post['nbshow'])) { $filters->show = (int)$post['nbshow']; } } $this->session->$varname = $filters; 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 integer $id Member ID * @param integer $fid Dynamic fields ID * @param integer $pos Dynamic field position * @param string $name File name * * @return Response */ public function getDynamicFile( Request $request, Response $response, int $id, int $fid, int $pos, string $name ): Response { $member = new Adherent($this->zdb); $member ->disableAllDeps() ->enableDep('dynamics') ->load($id); $denied = null; if (!$member->canShow($this->login)) { $fields = $member->getDynamicFields()->getFields(); if (!isset($fields[$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 ->withHeader( 'Location', $this->router->pathFor( 'member', ['id' => $id] ) ); } $filename = str_replace( [ '%mid', '%fid', '%pos' ], [ $id, $fid, $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-Description', 'File Transfer') ->withHeader('Content-Type', $type) ->withHeader('Content-Disposition', 'attachment;filename="' . $name . '"') ->withHeader('Pragma', 'no-cache') ->withHeader('Content-Transfer-Encoding', 'binary') ->withHeader('Expires', '0') ->withHeader('Cache-Control', 'must-revalidate') ->withHeader('Pragma', 'public'); $stream = fopen('php://memory', 'r+'); fwrite($stream, file_get_contents(GALETTE_FILES_PATH . $filename)); rewind($stream); return $response->withBody(new \Slim\Http\Stream($stream)); } else { Analog::log( 'A request has been made to get a dynamic 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 ->withHeader( 'Location', $this->router->pathFor('member', ['id' => $id]) ); } } /** * Members list * * @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, $option = null, $value = null): Response { if (isset($this->session->filter_members)) { $filters = $this->session->filter_members; } else { $filters = new MembersList(); } if ($option !== null) { switch ($option) { case 'page': $filters->current_page = (int)$value; break; case 'order': $filters->orderby = $value; break; } } $members = new Members($filters); $members_list = array(); if ($this->login->isAdmin() || $this->login->isStaff()) { $members_list = $members->getMembersList(true); } else { $members_list = $members->getManagedMembersList(true); } $groups = new Groups($this->zdb, $this->login); $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()); $this->session->filter_members = $filters; // display page $this->view->render( $response, 'gestion_adherents.tpl', array( 'page_title' => _T("Members management"), 'require_mass' => true, 'members' => $members_list, 'filter_groups_options' => $groups_list, 'nb_members' => $members->getCount(), 'filters' => $filters, 'adv_filters' => $filters instanceof AdvancedMembersList, 'galette_list' => $this->lists_config->getDisplayElements($this->login) ) ); return $response; } /** * Members filtering * * @param Request $request PSR Request * @param Response $response PSR Response * * @return 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(); } //reintialize 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); return $response ->withStatus(301) ->withHeader('Location', $this->router->pathFor('advanced-search')); } elseif (isset($post['adv_criteria'])) { return $response ->withStatus(301) ->withHeader('Location', $this->router->pathFor('advanced-search')); } else { //string to filter if (isset($post['filter_str'])) { //filter search string $filters->filter_str = stripslashes( htmlspecialchars($post['filter_str'], ENT_QUOTES) ); } //field to filter if (isset($post['field_filter'])) { if (is_numeric($post['field_filter'])) { $filters->field_filter = $post['field_filter']; } } //membership to filter if (isset($post['membership_filter'])) { if (is_numeric($post['membership_filter'])) { $filters->membership_filter = $post['membership_filter']; } } //account status to filter if (isset($post['filter_account'])) { if (is_numeric($post['filter_account'])) { $filters->filter_account = $post['filter_account']; } } //email filter if (isset($post['email_filter'])) { $filters->email_filter = (int)$post['email_filter']; } //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 = (int)$post['nbshow']; } if (isset($post['advanced_filtering'])) { if (!$filters instanceof AdvancedMembersList) { $filters = new AdvancedMembersList($filters); } //Advanced filters $filters->reinit(); unset($post['advanced_filtering']); $freed = false; foreach ($post as $k => $v) { if (strpos($k, 'free_', 0) === 0) { if (!$freed) { $i = 0; foreach ($post['free_field'] as $f) { if ( trim($f) !== '' && trim($post['free_text'][$i]) !== '' ) { $fs_search = htmlspecialchars($post['free_text'][$i], ENT_QUOTES); $log_op = (int)$post['free_logical_operator'][$i]; $qry_op = (int)$post['free_query_operator'][$i]; $type = (int)$post['free_type'][$i]; $fs = array( 'idx' => $i, 'field' => $f, 'type' => $type, 'search' => $fs_search, 'log_op' => $log_op, 'qry_op' => $qry_op ); $filters->free_search = $fs; } $i++; } $freed = true; } } elseif ($k == 'groups_search') { $i = 0; $filters->groups_search_log_op = (int)$post['groups_logical_operator']; foreach ($post['groups_search'] as $g) { if (trim($g) !== '') { $gs = array( 'idx' => $i, 'group' => $g ); $filters->groups_search = $gs; } $i++; } } else { switch ($k) { case 'contrib_min_amount': case 'contrib_max_amount': if (trim($v) !== '') { $v = (float)$v; } else { $v = null; } break; } $filters->$k = $v; } } } } if (isset($post['savesearch'])) { return $response ->withStatus(301) ->withHeader( 'Location', $this->router->pathFor( 'saveSearch', $post ) ); } $this->session->filter_members = $filters; return $response ->withStatus(301) ->withHeader('Location', $this->router->pathFor('members')); } /** * Advanced search page * * @param Request $request PSR Request * @param Response $response PSR Response * * @return Response */ public function advancedSearch(Request $request, Response $response): Response { if (isset($this->session->filter_members)) { $filters = $this->session->filter_members; if (!$filters instanceof AdvancedMembersList) { $filters = new AdvancedMembersList($filters); } } else { $filters = new AdvancedMembersList(); } $groups = new Groups($this->zdb, $this->login); $groups_list = $groups->getList(); //we want only visible fields $fields = $this->members_fields; $fc = $this->fields_config; $fc->filterVisible($this->login, $fields); //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); } //dynamic fields $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); $contrib_dynamics = new DynamicFieldsHandle($this->zdb, $this->login, $contrib); //Status $statuts = new Status($this->zdb); //Contributions types $ct = new ContributionsTypes($this->zdb); //Payment types $ptypes = new PaymentTypes( $this->zdb, $this->preferences, $this->login ); $ptlist = $ptypes->getList(); $filters->setViewCommonsFilters($this->preferences, $this->view->getSmarty()); // display page $this->view->render( $response, 'advanced_search.tpl', array( 'page_title' => _T("Advanced search"), 'filter_groups_options' => $groups_list, 'search_fields' => $fields, 'adh_dynamics' => $adh_dynamics->getFields(), 'contrib_dynamics' => $contrib_dynamics->getFields(), 'statuts' => $statuts->getList(), 'contributions_types' => $ct->getList(), 'filters' => $filters, 'payments_types' => $ptlist ) ); return $response; } /** * Members list for ajax * * @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, string $option = null, $value = null): Response { $post = $request->getParsedBody(); $filters = $this->session->ajax_members_filters ?? new MembersList(); if ($option == 'page') { $filters->current_page = (int)$value; } //numbers of rows to display if (isset($post['nbshow']) && is_numeric($post['nbshow'])) { $filters->show = (int)$post['nbshow']; } $members = new Members($filters); if (!$this->login->isAdmin() && !$this->login->isStaff()) { if ($this->login->isGroupManager()) { $members_list = $members->getManagedMembersList(true); } else { Analog::log( str_replace( ['%id', '%login'], [$this->login->id, $this->login->login], 'Trying to list group members without access from #%id (%login)' ), Analog::ERROR ); throw new \Exception('Access denied.'); } } else { $members_list = $members->getMembersList(true); } //assign pagination variables to the template and add pagination links $filters->setSmartyPagination($this->router, $this->view->getSmarty(), false); $this->session->ajax_members_filters = $filters; $selected_members = null; $unreachables_members = null; if (!isset($post['from'])) { $mailing = $this->session->mailing; if (!isset($post['members'])) { $selected_members = $mailing->recipients; $unreachables_members = $mailing->unreachables; } else { $m = new Members(); $selected_members = $m->getArrayList($post['members']); if (isset($post['unreachables']) && is_array($post['unreachables'])) { $unreachables_members = $m->getArrayList($post['unreachables']); } } } else { switch ($post['from']) { case 'groups': if (!isset($post['gid'])) { Analog::log( 'Trying to list group members with no group id provided', Analog::ERROR ); throw new \Exception('A group id is required.'); } if (!isset($post['members'])) { $group = new Group((int)$post['gid']); $selected_members = array(); if (!isset($post['mode']) || $post['mode'] == 'members') { $selected_members = $group->getMembers(); } elseif ($post['mode'] == 'managers') { $selected_members = $group->getManagers(); } else { Analog::log( 'Trying to list group members with unknown mode', Analog::ERROR ); throw new \Exception('Unknown mode.'); } } else { $m = new Members(); $selected_members = $m->getArrayList($post['members']); if (isset($post['unreachables']) && is_array($post['unreachables'])) { $unreachables_members = $m->getArrayList($post['unreachables']); } } break; case 'attach': if (!isset($post['id_adh'])) { throw new \RuntimeException( 'Current selected member must be excluded while attaching!' ); } break; } } $params = [ 'filters' => $filters, 'members_list' => $members_list, 'selected_members' => $selected_members, 'unreachables_members' => $unreachables_members ]; if (isset($post['multiple'])) { $params['multiple'] = true; } if (isset($post['gid'])) { $params['the_id'] = (int)$post['gid']; } if (isset($post['id_adh'])) { $params['excluded'] = (int)$post['id_adh']; } // display page $this->view->render( $response, 'ajax_members.tpl', $params ); return $response; } /** * Batch actions handler * * @param Request $request PSR Request * @param Response $response PSR Response * * @return 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; } 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')); } if (isset($post['masschange'])) { return $response ->withStatus(301) ->withHeader('Location', $this->router->pathFor('masschangeMembers')); } if (isset($post['masscontributions'])) { return $response ->withStatus(301) ->withHeader('Location', $this->router->pathFor('massAddContributionsChooseType')); } throw new \RuntimeException('Does not know what to batch :('); } else { $this->flash->addMessage( 'error_detected', _T("No member was selected, please check at least one name.") ); return $response ->withStatus(301) ->withHeader('Location', $this->router->pathFor('members')); } } // /CRUD - Read // CRUD - Update /** * Edit page * * @param Request $request PSR Request * @param Response $response PSR Response * @param mixed $id Member id/array of members id * @param string $action null or 'add' * * @return Response */ public function edit( Request $request, Response $response, int $id = null, string $action = 'edit' ): Response { //instantiate member object $member = new Adherent($this->zdb); $member->enableAllDeps()->load($id); if ($this->session->member !== null) { //retrieve from session, in add or edit $member = $this->session->member; $this->session->member = null; $id = $member->id; } if ($id !== null) { //load requested member $member->load($id); $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->router->pathFor('me') ); } //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 ($id === null) { $fc->setNotRequired('mdp_adh'); } //handle requirements for parent fields $parent_fields = $member->getParentFields(); $tpl_parent_fields = []; //for JS when detaching foreach ($parent_fields as $field) { if ($fc->isRequired($field)) { $tpl_parent_fields[] = $field; if ($member->hasParent()) { $fc->setNotRequired($field); } } } // flagging required fields invisible to members if ($this->login->isAdmin() || $this->login->isStaff()) { $fc->setNotRequired('activite_adh'); $fc->setNotRequired('id_statut'); } // template variable declaration $title = _T("Member Profile"); if ($member->id != '') { $title .= ' (' . _T("modification") . ')'; } else { $title .= ' (' . _T("creation") . ')'; } //Status $statuts = new Status($this->zdb); //Groups $groups = new Groups($this->zdb, $this->login); $groups_list = $groups->getSimpleList(true); $form_elements = $fc->getFormElements( $this->login, $id === null ); // members $m = new Members(); $pid = null; if ($member->hasParent()) { $pid = ($member->parent instanceof Adherent ? $member->parent->id : $member->parent); } $members = $m->getSelectizedMembers( $this->zdb, $pid ); $route_params['members'] = [ 'filters' => $m->getFilters(), 'count' => $m->getCount() ]; if (count($members)) { $route_params['members']['list'] = $members; } // display page $this->view->render( $response, 'member.tpl', array( 'parent_tpl' => 'page.tpl', 'autocomplete' => true, 'page_title' => $title, 'member' => $member, 'self_adh' => false, // pseudo random int 'time' => time(), 'titles_list' => Titles::getList($this->zdb), 'statuts' => $statuts->getList(), 'groups' => $groups_list, 'fieldsets' => $form_elements['fieldsets'], 'hidden_elements' => $form_elements['hiddens'], 'parent_fields' => $tpl_parent_fields, 'addchild' => ($action === 'addchild') ) + $route_params ); return $response; } /** * Edit action * * @param Request $request PSR Request * @param Response $response PSR Response * @param integer $id Member id * * @return Response */ public function doEdit(Request $request, Response $response, int $id): Response { return $this->store($request, $response); } /** * Massive change page * * @param Request $request PSR Request * @param Response $response PSR Response * * @return Response */ public function massChange(Request $request, Response $response): Response { $filters = $this->session->filter_members; $data = [ 'id' => $filters->selected, 'redirect_uri' => $this->router->pathFor('members') ]; $fc = $this->fields_config; $form_elements = $fc->getMassiveFormElements($this->members_fields, $this->login); //dynamic fields $member = new Adherent($this->zdb); $member->disableAllDeps()->enableDep('dynamics'); //Status $statuts = new Status($this->zdb); // display page $this->view->render( $response, 'mass_change_members.tpl', array( 'mode' => $request->isXhr() ? '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'), 'data' => $data, 'member' => $member, 'fieldsets' => $form_elements['fieldsets'], 'titles_list' => Titles::getList($this->zdb), 'statuts' => $statuts->getList(), 'require_mass' => true ) ); return $response; } /** * Massive changes validation page * * @param Request $request PSR Request * @param Response $response PSR Response * * @return Response */ public function validateMassChange(Request $request, Response $response): Response { $post = $request->getParsedBody(); if (!isset($post['confirm'])) { $this->flash->addMessage( 'error_detected', _T("Mass changes has not been confirmed!") ); } else { //we want only visibles fields $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['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] ?? 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; $data = [ 'id' => $filters->selected, 'redirect_uri' => $this->router->pathFor('members') ]; //Status $statuts = new Status($this->zdb); // display page $this->view->render( $response, 'mass_change_members.tpl', array( 'mode' => $request->isXhr() ? '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'), 'data' => $data, 'titles_list' => Titles::getList($this->zdb), 'statuts' => $statuts->getList(), 'changes' => $changes ) ); return $response; } /** * Do massive changes * * @param Request $request PSR Request * @param Response $response PSR Response * * @return 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'])) { $error_detected[] = _T("Mass changes has not been confirmed!"); } else { unset($post['confirm']); $ids = $post['id']; unset($post['id']); $fc = $this->fields_config; $form_elements = $fc->getMassiveFormElements($this->members_fields, $this->login); $disabled = $this->members_fields; foreach (array_keys($post) as $key) { $found = false; foreach ($form_elements['fieldsets'] as $fieldset) { if (isset($fieldset->elements[$key])) { $found = true; 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, Analog::WARNING ); unset($post[$key]); } else { unset($disabled[$key]); } } if (!count($post)) { $error_detected[] = _T("Nothing to do!"); } else { foreach ($ids as $id) { $is_manager = !$this->login->isAdmin() && !$this->login->isStaff() && $this->login->isGroupManager(); $member = new Adherent($this->zdb); $member->disableAllDeps(); if ($is_manager) { $member->enableDep('groups'); } $member->load((int)$id); $member->setDependencies( $this->preferences, $this->members_fields, $this->history ); if (!$member->canEdit($this->login)) { continue; } $valid = $member->check($post, [], $disabled); if ($valid === true) { $done = $member->store(); if (!$done) { $error_detected[] = _T("An error occurred while storing the member."); } else { ++$mass; } } else { $error_detected = array_merge($error_detected, $valid); } } } } if ($mass == 0 && !count($error_detected)) { $error_detected[] = _T('Something went wront during mass edition!'); } else { $this->flash->addMessage( 'success_detected', str_replace( '%count', $mass, _T('%count members has been changed successfully!') ) ); } if (count($error_detected) > 0) { foreach ($error_detected as $error) { $this->flash->addMessage( 'error_detected', $error ); } } if (!$request->isXhr()) { return $response ->withStatus(301) ->withHeader('Location', $redirect_url); } else { return $response->withJson( [ 'success' => count($error_detected) === 0 ] ); } } /** * Store * * @param Request $request PSR Request * @param Response $response PSR Response * * @return 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')); } $post = $request->getParsedBody(); $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 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 { if ($member->id != '') { $member->load($this->login->id); } } // flagging required fields $fc = $this->fields_config; // 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']) || isset($post['parent_id']) && !empty($post['parent_id']) ) { $parent_fields = $member->getParentFields(); foreach ($parent_fields as $field) { if ($fc->isRequired($field)) { $fc->setNotRequired($field); } } } // flagging required fields invisible to members if ($this->login->isAdmin() || $this->login->isStaff()) { $fc->setNotRequired('activite_adh'); $fc->setNotRequired('id_statut'); } $form_elements = $fc->getFormElements( $this->login, $member->id == '', $this->isSelfMembership() ); $fieldsets = $form_elements['fieldsets']; $required = array(); $disabled = array(); foreach ($fieldsets as $category) { foreach ($category->elements as $field) { if ($field->required == true) { $required[$field->field_id] = true; } if ($field->disabled == true) { $disabled[$field->field_id] = true; } elseif (!isset($post[$field->field_id])) { switch ($field->field_id) { //unchecked booleans are not sent from form case 'bool_admin_adh': case 'bool_exempt_adh': case 'bool_display_info': $post[$field->field_id] = 0; break; } } } } $real_requireds = array_diff(array_keys($required), array_keys($disabled)); // Validation $redirect_url = $this->router->pathFor('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) { $error_detected = array_merge($error_detected, $valid); } if (count($error_detected) == 0) { //all goes well, we can proceed $new = false; if ($member->id == '') { $new = true; } // send email to member if ($this->isSelfMembership() || isset($post['mail_confirm']) && $post['mail_confirm'] == '1') { $member->setSendmail(); //flag to send creation email } $store = $member->store(); if ($store === true) { //member has been stored :) if ($new) { if ($this->isSelfMembership()) { $success_detected[] = _T("Your account has been created!"); if ( $this->preferences->pref_mail_method > GaletteMail::METHOD_DISABLED && $member->getEmail() != '' ) { $success_detected[] = _T("An email has been sent to you, check your inbox."); } } else { $success_detected[] = _T("New member has been successfully added."); } } else { $success_detected[] = _T("Member account has been modified."); } //store requested groups $groups_adh = $post['groups_adh'] ?? null; $managed_groups_adh = $post['groups_managed_adh'] ?? null; //add/remove user from groups $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 $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 (is_array($files_res)) { $error_detected = array_merge($error_detected, $files_res); } if (isset($post['del_photo'])) { if (!$member->picture->delete($member->id)) { $error_detected[] = _T("Delete failed"); $str_adh = $member->id . ' (' . $member->sname . ' ' . ')'; Analog::log( 'Unable to delete picture for member ' . $str_adh, Analog::ERROR ); } } } if (count($error_detected) > 0) { 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]]); $error = str_replace( '%member_url_' . $matches[1], $url, $error ); } $this->flash->addMessage( 'error_detected', $error ); } } 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( 'success_detected', $success ); } } if (count($error_detected) === 0) { if ($this->isSelfMembership()) { $redirect_url = $this->router->pathFor('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('addTransaction'); break; case Adherent::AFTER_ADD_NEW: $redirect_url = $this->router->pathFor('addMember'); break; case Adherent::AFTER_ADD_SHOW: $redirect_url = $this->router->pathFor('member', ['id' => $member->id]); break; case Adherent::AFTER_ADD_LIST: $redirect_url = $this->router->pathFor('members'); break; case Adherent::AFTER_ADD_HOME: $redirect_url = $this->router->pathFor('slash'); break; } } elseif (!isset($post['id_adh']) && !$member->isDueFree()) { $redirect_url = $this->router->pathFor( 'addContribution', ['type' => 'fee'] ) . '?id_adh=' . $member->id; } else { $redirect_url = $this->router->pathFor('member', ['id' => $member->id]); } } else { //store entity in session $this->session->member = $member; if ($this->isSelfMembership()) { $redirect_url = $this->router->pathFor('subscribe'); } else { if ($member->id) { $redirect_url = $this->router->pathFor( 'editMember', ['id' => $member->id] ); } else { $redirect_url = $this->router->pathFor((isset($post['addchild']) ? 'addMemberChild' : 'addMember')); } } } } return $response ->withStatus(301) ->withHeader('Location', $redirect_url); } // /CRUD - Update // CRUD - Delete /** * Get redirection URI * * @param array $args Route arguments * * @return string */ public function redirectUri(array $args) { return $this->router->pathFor('members'); } /** * Get form URI * * @param array $args Route arguments * * @return string */ public function formUri(array $args) { return $this->router->pathFor( '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 * * @param array $args Route arguments * * @return string */ public function confirmRemoveTitle(array $args) { if (isset($args['id_adh']) || isset($args['id'])) { //one member removal $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; return str_replace( '%count', count($filters->selected), _T('You are about to remove %count members.') ); } } /** * Remove object * * @param array $args Route arguments * @param array $post POST values * * @return boolean */ protected function doDelete(array $args, array $post) { if (isset($this->session->filter_members)) { $filters = $this->session->filter_members; } else { $filters = new MembersList(); } $members = new Members($filters); if (!is_array($post['id'])) { $ids = (array)$post['id']; } else { $ids = $post['id']; } return $members->removeMembers($ids); } // CRUD - Delete /** * Set self memebrship flag * * @return MembersController */ private function setSelfMembership(): MembersController { $this->is_self_membership = true; return $this; } /** * Is self membership? * * @return boolean */ private function isSelfMembership(): bool { return $this->is_self_membership; } }