diff options
17 files changed, 790 insertions, 58 deletions
diff --git a/app/Functions/FunctionsEdit.php b/app/Functions/FunctionsEdit.php index 1d9e5c17c9..68e65076dd 100644 --- a/app/Functions/FunctionsEdit.php +++ b/app/Functions/FunctionsEdit.php @@ -37,6 +37,9 @@ use Fisharebest\Webtrees\GedcomCode\GedcomCodeTemp; use Fisharebest\Webtrees\GedcomRecord; use Fisharebest\Webtrees\GedcomTag; use Fisharebest\Webtrees\Html; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateRepositoryModal; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSourceModal; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSubmitterModal; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Media; @@ -524,7 +527,7 @@ class FunctionsEdit } elseif ($fact === 'REPO') { $html .= '<div class="input-group">' . - '<div class="input-group-prepend"><button class="btn btn-secondary" type="button" data-toggle="modal" data-href="' . e(route('create-repository', ['tree' => $tree->name()])) . '" data-target="#wt-ajax-modal" data-select-id="' . $id . '" title="' . I18N::translate('Create a repository') . '">' . view('icons/add') . '</button></div>' . + '<div class="input-group-prepend"><button class="btn btn-secondary" type="button" data-toggle="modal" data-href="' . e(route(CreateRepositoryModal::class, ['tree' => $tree->name()])) . '" data-target="#wt-ajax-modal" data-select-id="' . $id . '" title="' . I18N::translate('Create a repository') . '">' . view('icons/add') . '</button></div>' . view('components/select-repository', ['id' => $id, 'name' => $name, 'repository' => Repository::getInstance($value, $tree), 'tree' => $tree]) . '</div>'; } elseif ($fact === 'RESN') { @@ -541,7 +544,7 @@ class FunctionsEdit } elseif ($fact === 'SOUR') { $html .= '<div class="input-group">' . - '<div class="input-group-prepend"><button class="btn btn-secondary" type="button" data-toggle="modal" data-href="' . e(route('create-source', ['tree' => $tree->name()])) . '" data-target="#wt-ajax-modal" data-select-id="' . $id . '" title="' . I18N::translate('Create a source') . '">' . view('icons/add') . '</button></div>' . + '<div class="input-group-prepend"><button class="btn btn-secondary" type="button" data-toggle="modal" data-href="' . e(route(CreateSourceModal::class, ['tree' => $tree->name()])) . '" data-target="#wt-ajax-modal" data-select-id="' . $id . '" title="' . I18N::translate('Create a source') . '">' . view('icons/add') . '</button></div>' . view('components/select-source', ['id' => $id, 'name' => $name, 'source' => Source::getInstance($value, $tree), 'tree' => $tree]) . '</div>'; } elseif ($fact === 'STAT') { @@ -549,7 +552,7 @@ class FunctionsEdit } elseif ($fact === 'SUBM') { $html .= '<div class="input-group">' . - '<div class="input-group-prepend"><button class="btn btn-secondary" type="button" data-toggle="modal" data-href="' . e(route('create-submitter', ['tree' => $tree->name()])) . '" data-target="#wt-ajax-modal" data-select-id="' . $id . '" title="' . I18N::translate('Create a submitter') . '">' . view('icons/add') . '</button></div>' . + '<div class="input-group-prepend"><button class="btn btn-secondary" type="button" data-toggle="modal" data-href="' . e(route(CreateSubmitterModal::class, ['tree' => $tree->name()])) . '" data-target="#wt-ajax-modal" data-select-id="' . $id . '" title="' . I18N::translate('Create a submitter') . '">' . view('icons/add') . '</button></div>' . view('components/select-submitter', ['id' => $id, 'name' => $name, 'submitter' => GedcomRecord::getInstance($value, $tree), 'tree' => $tree]) . '</div>'; } elseif ($fact === 'TEMP') { diff --git a/app/Http/RequestHandlers/CreateRepositoryAction.php b/app/Http/RequestHandlers/CreateRepositoryAction.php new file mode 100644 index 0000000000..41526e6540 --- /dev/null +++ b/app/Http/RequestHandlers/CreateRepositoryAction.php @@ -0,0 +1,79 @@ +<?php + +/** + * webtrees: online genealogy + * Copyright (C) 2019 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Http\RequestHandlers; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Tree; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +use function assert; + +/** + * Process a form to create a new repository. + */ +class CreateRepositoryAction implements RequestHandlerInterface +{ + /** + * @param ServerRequestInterface $request + * + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $tree = $request->getAttribute('tree'); + assert($tree instanceof Tree); + + $params = $request->getParsedBody(); + $name = $params['repository-name']; + $privacy_restriction = $params['privacy-restriction']; + $edit_restriction = $params['edit-restriction']; + + // Fix whitespace + $name = trim(preg_replace('/\s+/', ' ', $name)); + + $gedcom = "0 @@ REPO\n1 NAME " . $name; + + if (in_array($privacy_restriction, ['none', 'privacy', 'confidential'], true)) { + $gedcom .= "\n1 RESN " . $privacy_restriction; + } + + if ($edit_restriction === 'locked') { + $gedcom .= "\n1 RESN " . $edit_restriction; + } + + $record = $tree->createRecord($gedcom); + + // id and text are for select2 / autocomplete + // html is for interactive modals + return response([ + 'id' => $record->xref(), + 'text' => view('selects/repository', [ + 'repository' => $record, + ]), + 'html' => view('modals/record-created', [ + 'title' => I18N::translate('The repository has been created'), + 'name' => $record->fullName(), + 'url' => $record->url(), + ]), + ]); + } +} diff --git a/app/Http/Controllers/EditRepositoryController.php b/app/Http/RequestHandlers/CreateRepositoryModal.php index 16a392d729..def6c0b3f3 100644 --- a/app/Http/Controllers/EditRepositoryController.php +++ b/app/Http/RequestHandlers/CreateRepositoryModal.php @@ -17,30 +17,34 @@ declare(strict_types=1); -namespace Fisharebest\Webtrees\Http\Controllers; +namespace Fisharebest\Webtrees\Http\RequestHandlers; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Tree; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; use function assert; /** - * Controller for edit forms and responses. + * Show a form to create a new repository. */ -class EditRepositoryController extends AbstractEditController +class CreateRepositoryModal implements RequestHandlerInterface { /** - * Show a form to create a new repository. - * * @param ServerRequestInterface $request * * @return ResponseInterface */ - public function createRepository(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { - return response(view('modals/create-repository')); + $tree = $request->getAttribute('tree'); + assert($tree instanceof Tree); + + return response(view('modals/create-repository', [ + 'tree' => $tree, + ])); } /** diff --git a/app/Http/RequestHandlers/CreateSourceAction.php b/app/Http/RequestHandlers/CreateSourceAction.php new file mode 100644 index 0000000000..e76fbfceaf --- /dev/null +++ b/app/Http/RequestHandlers/CreateSourceAction.php @@ -0,0 +1,121 @@ +<?php + +/** + * webtrees: online genealogy + * Copyright (C) 2019 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Http\RequestHandlers; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Tree; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +use function assert; + +/** + * Process a form to create a new source. + */ +class CreateSourceAction implements RequestHandlerInterface +{ + /** + * @param ServerRequestInterface $request + * + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $tree = $request->getAttribute('tree'); + assert($tree instanceof Tree); + + $params = $request->getParsedBody(); + $title = $params['source-title']; + $abbreviation = $params['source-abbreviation']; + $author = $params['source-author']; + $publication = $params['source-publication']; + $repository = $params['source-repository']; + $call_number = $params['source-call-number']; + $text = $params['source-text']; + $privacy_restriction = $params['privacy-restriction']; + $edit_restriction = $params['edit-restriction']; + + // Fix whitespace + $title = trim(preg_replace('/\s+/', ' ', $title)); + $abbreviation = trim(preg_replace('/\s+/', ' ', $abbreviation)); + $author = trim(preg_replace('/\s+/', ' ', $author)); + $publication = trim(preg_replace('/\s+/', ' ', $publication)); + $repository = trim(preg_replace('/\s+/', ' ', $repository)); + $call_number = trim(preg_replace('/\s+/', ' ', $call_number)); + + // Convert line endings to GEDDCOM continuations + $text = str_replace([ + "\r\n", + "\r", + "\n", + ], "\n1 CONT ", $text); + + $gedcom = "0 @@ SOUR\n\n1 TITL " . $title; + + if ($abbreviation !== '') { + $gedcom .= "\n1 ABBR " . $abbreviation; + } + + if ($author !== '') { + $gedcom .= "\n1 AUTH " . $author; + } + + if ($publication !== '') { + $gedcom .= "\n1 PUBL " . $publication; + } + + if ($text !== '') { + $gedcom .= "\n1 TEXT " . $text; + } + + if ($repository !== '') { + $gedcom .= "\n1 REPO @" . $repository . '@'; + + if ($call_number !== '') { + $gedcom .= "\n2 CALN " . $call_number; + } + } + + if (in_array($privacy_restriction, ['none', 'privacy', 'confidential'], true)) { + $gedcom .= "\n1 RESN " . $privacy_restriction; + } + + if ($edit_restriction === 'locked') { + $gedcom .= "\n1 RESN " . $edit_restriction; + } + + $record = $tree->createRecord($gedcom); + + // id and text are for select2 / autocomplete + // html is for interactive modals + return response([ + 'id' => $record->xref(), + 'text' => view('selects/source', [ + 'source' => $record, + ]), + 'html' => view('modals/record-created', [ + 'title' => I18N::translate('The source has been created'), + 'name' => $record->fullName(), + 'url' => $record->url(), + ]), + ]); + } +} diff --git a/app/Http/Controllers/EditSourceController.php b/app/Http/RequestHandlers/CreateSourceModal.php index e300e37fa3..2e29b15431 100644 --- a/app/Http/Controllers/EditSourceController.php +++ b/app/Http/RequestHandlers/CreateSourceModal.php @@ -17,35 +17,37 @@ declare(strict_types=1); -namespace Fisharebest\Webtrees\Http\Controllers; +namespace Fisharebest\Webtrees\Http\RequestHandlers; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Tree; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; use function assert; /** - * Controller for edit forms and responses. + * Process a form to create a new source. */ -class EditSourceController extends AbstractEditController +class CreateSourceModal implements RequestHandlerInterface { /** - * Show a form to create a new source. - * * @param ServerRequestInterface $request * * @return ResponseInterface */ - public function createSource(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { - return response(view('modals/create-source')); + $tree = $request->getAttribute('tree'); + assert($tree instanceof Tree); + + return response(view('modals/create-source', [ + 'tree' => $tree, + ])); } /** - * Process a form to create a new source. - * * @param ServerRequestInterface $request * * @return ResponseInterface diff --git a/app/Http/Controllers/EditSubmitterController.php b/app/Http/RequestHandlers/CreateSubmitterAction.php index 4241d29f6c..ee76278d13 100644 --- a/app/Http/Controllers/EditSubmitterController.php +++ b/app/Http/RequestHandlers/CreateSubmitterAction.php @@ -17,40 +17,27 @@ declare(strict_types=1); -namespace Fisharebest\Webtrees\Http\Controllers; +namespace Fisharebest\Webtrees\Http\RequestHandlers; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Tree; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; use function assert; /** - * Controller for edit forms and responses. + * Process a form to create a new submitter. */ -class EditSubmitterController extends AbstractEditController +class CreateSubmitterAction implements RequestHandlerInterface { /** - * Show a form to create a new submitter. - * - * @param ServerRequestInterface $request - * - * @return ResponseInterface - */ - public function createSubmitter(ServerRequestInterface $request): ResponseInterface - { - return response(view('modals/create-submitter')); - } - - /** - * Process a form to create a new submitter. - * * @param ServerRequestInterface $request * * @return ResponseInterface */ - public function createSubmitterAction(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $tree = $request->getAttribute('tree'); assert($tree instanceof Tree); diff --git a/app/Http/RequestHandlers/CreateSubmitterModal.php b/app/Http/RequestHandlers/CreateSubmitterModal.php new file mode 100644 index 0000000000..a5bc552cab --- /dev/null +++ b/app/Http/RequestHandlers/CreateSubmitterModal.php @@ -0,0 +1,48 @@ +<?php + +/** + * webtrees: online genealogy + * Copyright (C) 2019 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Http\RequestHandlers; + +use Fisharebest\Webtrees\Tree; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +use function assert; + +/** + * Show a form to create a new submitter. + */ +class CreateSubmitterModal implements RequestHandlerInterface +{ + /** + * @param ServerRequestInterface $request + * + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $tree = $request->getAttribute('tree'); + assert($tree instanceof Tree); + + return response(view('modals/create-submitter', [ + 'tree' => $tree, + ])); + } +} diff --git a/app/Http/RequestHandlers/IndividualPage.php b/app/Http/RequestHandlers/IndividualPage.php new file mode 100644 index 0000000000..f46da95f4b --- /dev/null +++ b/app/Http/RequestHandlers/IndividualPage.php @@ -0,0 +1,395 @@ +<?php + +/** + * webtrees: online genealogy + * Copyright (C) 2019 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Http\RequestHandlers; + +use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\Date; +use Fisharebest\Webtrees\Fact; +use Fisharebest\Webtrees\Functions\FunctionsDate; +use Fisharebest\Webtrees\Functions\FunctionsPrint; +use Fisharebest\Webtrees\Functions\FunctionsPrintFacts; +use Fisharebest\Webtrees\GedcomCode\GedcomCodeName; +use Fisharebest\Webtrees\GedcomTag; +use Fisharebest\Webtrees\Http\ViewResponseTrait; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Individual; +use Fisharebest\Webtrees\Media; +use Fisharebest\Webtrees\MediaFile; +use Fisharebest\Webtrees\Module\ModuleSidebarInterface; +use Fisharebest\Webtrees\Module\ModuleTabInterface; +use Fisharebest\Webtrees\Services\ClipboardService; +use Fisharebest\Webtrees\Services\ModuleService; +use Fisharebest\Webtrees\Services\UserService; +use Fisharebest\Webtrees\Tree; +use Illuminate\Support\Collection; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use stdClass; + +use function assert; +use function e; +use function explode; +use function is_string; +use function ob_get_clean; +use function ob_start; +use function preg_match_all; +use function preg_replace; +use function redirect; +use function route; +use function str_replace; +use function strpos; + +/** + * Show an individual's page. + */ +class IndividualPage implements RequestHandlerInterface +{ + use ViewResponseTrait; + + /** @var ClipboardService */ + private $clipboard_service; + + /** @var ModuleService */ + private $module_service; + + /** @var UserService */ + private $user_service; + + /** + * IndividualController constructor. + * + * @param ClipboardService $clipboard_service + * @param ModuleService $module_service + * @param UserService $user_service + */ + public function __construct(ClipboardService $clipboard_service, ModuleService $module_service, UserService $user_service) + { + $this->clipboard_service = $clipboard_service; + $this->module_service = $module_service; + $this->user_service = $user_service; + } + + /** + * @param ServerRequestInterface $request + * + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $tree = $request->getAttribute('tree'); + assert($tree instanceof Tree); + + $xref = $request->getAttribute('xref'); + assert(is_string($xref)); + + $individual = Individual::getInstance($xref, $tree); + $individual = Auth::checkIndividualAccess($individual); + + // Redirect to correct xref/slug + if ($individual->xref() !== $xref || $request->getAttribute('slug') !== $individual->slug()) { + return redirect($individual->url()); + } + + // What is (was) the age of the individual + $bdate = $individual->getBirthDate(); + $ddate = $individual->getDeathDate(); + if ($bdate->isOK() && !$individual->isDead()) { + // If living display age + $age = ' (' . I18N::translate('age') . ' ' . FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($bdate, new Date(strtoupper(date('d M Y'))))) . ')'; + } elseif ($bdate->isOK() && $ddate->isOK()) { + // If dead, show age at death + $age = ' (' . I18N::translate('age') . ' ' . FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($bdate, $ddate)) . ')'; + } else { + $age = ''; + } + + // What images are linked to this individual + $individual_media = new Collection(); + foreach ($individual->facts(['OBJE']) as $fact) { + $media_object = $fact->target(); + if ($media_object instanceof Media) { + $media_file = $media_object->firstImageFile(); + if ($media_file instanceof MediaFile) { + $individual_media->add($media_file); + } + } + } + + $name_records = new Collection(); + foreach ($individual->facts(['NAME']) as $n => $name_fact) { + $name_records->add($this->formatNameRecord($tree, $n, $name_fact)); + } + + $sex_records = new Collection(); + foreach ($individual->facts(['SEX']) as $n => $sex_fact) { + $sex_records->add($this->formatSexRecord($sex_fact)); + } + + // If this individual is linked to a user account, show the link + $user_link = ''; + if (Auth::isAdmin()) { + $users = $this->user_service->findByIndividual($individual); + foreach ($users as $user) { + $user_link = ' — <a href="' . e(route('admin-users', ['filter' => $user->email()])) . '">' . e($user->userName()) . '</a>'; + } + } + + return $this->viewResponse('individual-page', [ + 'age' => $age, + 'clipboard_facts' => $this->clipboard_service->pastableFacts($individual, new Collection()), + 'count_media' => $this->countFacts($individual, ['OBJE']), + 'count_names' => $this->countFacts($individual, ['NAME']), + 'count_sex' => $this->countFacts($individual, ['SEX']), + 'individual' => $individual, + 'individual_media' => $individual_media, + 'meta_robots' => 'index,follow', + 'name_records' => $name_records, + 'sex_records' => $sex_records, + 'sidebars' => $this->getSidebars($individual), + 'tabs' => $this->getTabs($individual), + 'significant' => $this->significant($individual), + 'title' => $individual->fullName() . ' ' . $individual->getLifeSpan(), + 'tree' => $tree, + 'user_link' => $user_link, + ]); + } + + /** + * Count the (non-pending-delete) name records for an individual. + * + * @param Individual $individual + * @param string[] $tags + * + * @return int + */ + private function countFacts(Individual $individual, array $tags): int + { + $count = 0; + + foreach ($individual->facts($tags) as $fact) { + if (!$fact->isPendingDeletion()) { + $count++; + } + } + + return $count; + } + + /** + * Format a name record + * + * @param Tree $tree + * @param int $n + * @param Fact $fact + * + * @return string + */ + private function formatNameRecord(Tree $tree, $n, Fact $fact): string + { + $individual = $fact->record(); + + // Create a dummy record, so we can extract the formatted NAME value from it. + $dummy = new Individual( + 'xref', + "0 @xref@ INDI\n1 DEAT Y\n" . $fact->gedcom(), + null, + $individual->tree() + ); + $dummy->setPrimaryName(0); // Make sure we use the name from "1 NAME" + + $container_class = 'card'; + $content_class = 'collapse'; + $aria = 'false'; + + if ($n === 0) { + $content_class = 'collapse show'; + $aria = 'true'; + } + if ($fact->isPendingDeletion()) { + $container_class .= ' wt-old'; + } elseif ($fact->isPendingAddition()) { + $container_class .= ' wt-new'; + } + + ob_start(); + echo '<dl><dt class="label">', I18N::translate('Name'), '</dt>'; + echo '<dd class="field">', $dummy->fullName(), '</dd>'; + $ct = preg_match_all('/\n2 (\w+) (.*)/', $fact->gedcom(), $nmatch, PREG_SET_ORDER); + for ($i = 0; $i < $ct; $i++) { + $tag = $nmatch[$i][1]; + if ($tag !== 'SOUR' && $tag !== 'NOTE' && $tag !== 'SPFX') { + echo '<dt class="label">', GedcomTag::getLabel($tag, $individual), '</dt>'; + echo '<dd class="field">'; // Before using dir="auto" on this field, note that Gecko treats this as an inline element but WebKit treats it as a block element + if (isset($nmatch[$i][2])) { + $name = e($nmatch[$i][2]); + $name = str_replace('/', '', $name); + $name = preg_replace('/(\S*)\*/', '<span class="starredname">\\1</span>', $name); + switch ($tag) { + case 'TYPE': + echo GedcomCodeName::getValue($name, $individual); + break; + case 'SURN': + // The SURN field is not necessarily the surname. + // Where it is not a substring of the real surname, show it after the real surname. + $surname = e($dummy->getAllNames()[0]['surname']); + $surns = preg_replace('/, */', ' ', $nmatch[$i][2]); + if (strpos($dummy->getAllNames()[0]['surname'], $surns) !== false) { + echo '<span dir="auto">' . $surname . '</span>'; + } else { + echo I18N::translate('%1$s (%2$s)', '<span dir="auto">' . $surname . '</span>', '<span dir="auto">' . $name . '</span>'); + } + break; + default: + echo '<span dir="auto">' . $name . '</span>'; + break; + } + } + echo '</dd>'; + } + } + echo '</dl>'; + if (strpos($fact->gedcom(), "\n2 SOUR") !== false) { + echo '<div id="indi_sour" class="clearfix">', FunctionsPrintFacts::printFactSources($tree, $fact->gedcom(), 2), '</div>'; + } + if (strpos($fact->gedcom(), "\n2 NOTE") !== false) { + echo '<div id="indi_note" class="clearfix">', FunctionsPrint::printFactNotes($tree, $fact->gedcom(), 2), '</div>'; + } + $content = ob_get_clean(); + + if ($fact->canEdit()) { + $edit_links = + '<a class="btn btn-link" href="#" data-confirm="' . I18N::translate('Are you sure you want to delete this fact?') . '" data-post-url="' . e(route(DeleteFact::class, ['tree' => $individual->tree()->name(), 'xref' => $individual->xref(), 'fact_id' => $fact->id()])) . '" title="' . I18N::translate('Delete this name') . '">' . view('icons/delete') . '<span class="sr-only">' . I18N::translate('Delete this name') . '</span></a>' . + '<a class="btn btn-link" href="' . e(route('edit-name', ['xref' => $individual->xref(), 'fact_id' => $fact->id(), 'tree' => $individual->tree()->name()])) . '" title="' . I18N::translate('Edit the name') . '">' . view('icons/edit') . '<span class="sr-only">' . I18N::translate('Edit the name') . '</span></a>'; + } else { + $edit_links = ''; + } + + return ' + <div class="' . $container_class . '"> + <div class="card-header" role="tab" id="name-header-' . $n . '"> + <a data-toggle="collapse" data-parent="#individual-names" href="#name-content-' . $n . '" aria-expanded="' . $aria . '" aria-controls="name-content-' . $n . '">' . $dummy->fullName() . '</a> + ' . $edit_links . ' + </div> + <div id="name-content-' . $n . '" class="' . $content_class . '" role="tabpanel" aria-labelledby="name-header-' . $n . '"> + <div class="card-body">' . $content . '</div> + </div> + </div>'; + } + + /** + * print information for a sex record + * + * @param Fact $fact + * + * @return string + */ + private function formatSexRecord(Fact $fact): string + { + $individual = $fact->record(); + + switch ($fact->value()) { + case 'M': + $sex = I18N::translate('Male'); + break; + case 'F': + $sex = I18N::translate('Female'); + break; + default: + $sex = I18N::translateContext('unknown gender', 'Unknown'); + break; + } + + $container_class = 'card'; + if ($fact->isPendingDeletion()) { + $container_class .= ' wt-old'; + } elseif ($fact->isPendingAddition()) { + $container_class .= ' wt-new'; + } + + if ($individual->canEdit()) { + $edit_links = '<a class="btn btn-link" href="' . e(route(EditFact::class, ['xref' => $individual->xref(), 'fact_id' => $fact->id(), 'tree' => $individual->tree()->name()])) . '" title="' . I18N::translate('Edit the gender') . '">' . view('icons/edit') . '<span class="sr-only">' . I18N::translate('Edit the gender') . '</span></a>'; + } else { + $edit_links = ''; + } + + return ' + <div class="' . $container_class . '"> + <div class="card-header" role="tab" id="name-header-add"> + <div class="card-title mb-0"> + <b>' . I18N::translate('Gender') . '</b> ' . $sex . $edit_links . ' + </div> + </div> + </div>'; + } + + /** + * Which tabs should we show on this individual's page. + * We don't show empty tabs. + * + * @param Individual $individual + * + * @return Collection + */ + public function getSidebars(Individual $individual): Collection + { + return $this->module_service->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user()) + ->filter(static function (ModuleSidebarInterface $sidebar) use ($individual): bool { + return $sidebar->hasSidebarContent($individual); + }); + } + + /** + * Which tabs should we show on this individual's page. + * We don't show empty tabs. + * + * @param Individual $individual + * + * @return Collection + */ + public function getTabs(Individual $individual): Collection + { + return $this->module_service->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user()) + ->filter(static function (ModuleTabInterface $tab) use ($individual): bool { + return $tab->hasTabContent($individual); + }); + } + + /** + * What are the significant elements of this page? + * The layout will need them to generate URLs for charts and reports. + * + * @param Individual $individual + * + * @return stdClass + */ + private function significant(Individual $individual): stdClass + { + [$surname] = explode(',', $individual->sortName()); + + $family = $individual->childFamilies()->merge($individual->spouseFamilies())->first(); + + return (object) [ + 'family' => $family, + 'individual' => $individual, + 'surname' => $surname, + ]; + } +} diff --git a/app/Individual.php b/app/Individual.php index e61a0d3131..c1085fbafa 100644 --- a/app/Individual.php +++ b/app/Individual.php @@ -23,6 +23,7 @@ use Closure; use Exception; use Fisharebest\ExtCalendar\GregorianCalendar; use Fisharebest\Webtrees\GedcomCode\GedcomCodePedi; +use Fisharebest\Webtrees\Http\RequestHandlers\IndividualPage; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Support\Collection; use stdClass; @@ -34,7 +35,7 @@ class Individual extends GedcomRecord { public const RECORD_TYPE = 'INDI'; - protected const ROUTE_NAME = 'individual'; + protected const ROUTE_NAME = IndividualPage::class; /** @var int used in some lists to keep track of this individual’s generation in that list */ public $generation; diff --git a/app/Module/ModuleTabInterface.php b/app/Module/ModuleTabInterface.php index f44fd5b17d..333862d518 100644 --- a/app/Module/ModuleTabInterface.php +++ b/app/Module/ModuleTabInterface.php @@ -21,6 +21,8 @@ namespace Fisharebest\Webtrees\Module; use Fisharebest\Webtrees\Individual; use Illuminate\Support\Collection; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; /** * Interface ModuleTabInterface - Classes and libraries for module system @@ -98,4 +100,13 @@ interface ModuleTabInterface extends ModuleInterface * @return Collection */ public function supportedFacts(): Collection; + + /** + * Generate an AJAX response for a tab. + * + * @param ServerRequestInterface $request + * + * @return ResponseInterface + */ + public function getTabAction(ServerRequestInterface $request): ResponseInterface; } diff --git a/app/Module/ModuleTabTrait.php b/app/Module/ModuleTabTrait.php index d9b334dfda..6e18368be0 100644 --- a/app/Module/ModuleTabTrait.php +++ b/app/Module/ModuleTabTrait.php @@ -19,7 +19,17 @@ declare(strict_types=1); namespace Fisharebest\Webtrees\Module; +use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\Individual; +use Fisharebest\Webtrees\Tree; use Illuminate\Support\Collection; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; + +use function assert; +use function response; +use function view; /** * Trait ModuleTabTrait - default implementation of ModuleTabInterface @@ -35,6 +45,25 @@ trait ModuleTabTrait abstract public function title(): string; /** + * Get a the current access level for a module + * + * @param Tree $tree + * @param string $interface + * + * @return int + */ + abstract public function accessLevel(Tree $tree, string $interface): int; + + /** + * Generate the HTML content of this tab. + * + * @param Individual $individual + * + * @return string + */ + abstract public function getTabContent(Individual $individual): string; + + /** * The text that appears on the tab. * * @return string @@ -85,4 +114,32 @@ trait ModuleTabTrait { return new Collection([]); } + + /** + * @param ServerRequestInterface $request + * + * @return ResponseInterface + */ + public function getTabAction(ServerRequestInterface $request): ResponseInterface + { + $tree = $request->getAttribute('tree'); + assert($tree instanceof Tree); + + $xref = $request->getQueryParams()['xref']; + + $record = Individual::getInstance($xref, $tree); + $record = Auth::checkIndividualAccess($record); + + $user = $request->getAttribute('user'); + + if ($this->accessLevel($tree, 'tab') < Auth::accessLevel($tree, $user)) { + throw new AccessDeniedHttpException('Access denied'); + } + + $layout = view('layouts/ajax', [ + 'content' => $this->getTabContent($record), + ]); + + return response($layout); + } } diff --git a/resources/views/admin/trees.phtml b/resources/views/admin/trees.phtml index 1ec9170616..62fefd2a65 100644 --- a/resources/views/admin/trees.phtml +++ b/resources/views/admin/trees.phtml @@ -2,6 +2,9 @@ use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateRepositoryModal; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSourceModal; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSubmitterModal; use Fisharebest\Webtrees\Http\RequestHandlers\DeleteTreeAction; use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogPage; use Fisharebest\Webtrees\Http\RequestHandlers\SelectDefaultTree; @@ -215,7 +218,7 @@ use Illuminate\Database\Capsule\Manager as DB; <!-- UNLINKED SOURCE --> <li> <span class="fa-li"><?= view('icons/source') ?></span> - <a href="#" data-href="<?= e(route('create-source', ['tree' => $managed_tree->name()])) ?>" data-target="#wt-ajax-modal" data-toggle="modal"> + <a href="#" data-href="<?= e(route(CreateSourceModal::class, ['tree' => $managed_tree->name()])) ?>" data-target="#wt-ajax-modal" data-toggle="modal"> <?= I18N::translate('Source') ?> <span class="sr-only"> <?= e($managed_tree->title()) ?> @@ -225,7 +228,7 @@ use Illuminate\Database\Capsule\Manager as DB; <!-- UNLINKED REPOSITORY --> <li> <span class="fa-li"><?= view('icons/repository') ?></span> - <a href="#" data-href="<?= e(route('create-repository', ['tree' => $managed_tree->name()])) ?>" data-target="#wt-ajax-modal" data-toggle="modal"> + <a href="#" data-href="<?= e(route(CreateRepositoryModal::class, ['tree' => $managed_tree->name()])) ?>" data-target="#wt-ajax-modal" data-toggle="modal"> <?= I18N::translate('Repository') ?> <span class="sr-only"> <?= e($managed_tree->title()) ?> @@ -256,7 +259,7 @@ use Illuminate\Database\Capsule\Manager as DB; <!-- UNLINKED SUBMITTER --> <li> <span class="fa-li"><?= view('icons/submitter') ?></span> - <a href="#" data-href="<?= e(route('create-submitter', ['tree' => $managed_tree->name()])) ?>" data-target="#wt-ajax-modal" data-toggle="modal"> + <a href="#" data-href="<?= e(route(CreateSubmitterModal::class, ['tree' => $managed_tree->name()])) ?>" data-target="#wt-ajax-modal" data-toggle="modal"> <span class="sr-only"> <?= e($managed_tree->title()) ?> </span> diff --git a/resources/views/individual-page.phtml b/resources/views/individual-page.phtml index cd65e15b3f..8c63eb2b1b 100644 --- a/resources/views/individual-page.phtml +++ b/resources/views/individual-page.phtml @@ -1,4 +1,5 @@ <?php + use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Functions\FunctionsPrint; use Fisharebest\Webtrees\Http\RequestHandlers\AddNewFact; @@ -108,7 +109,7 @@ use Illuminate\Support\Collection; <ul class="nav nav-tabs flex-wrap"> <?php foreach ($tabs as $tab) : ?> <li class="nav-item"> - <a class="nav-link<?= $tab->isGrayedOut($individual) ? ' text-muted' : '' ?>" data-toggle="tab" role="tab" data-href="<?= e(route('individual-tab', ['xref' => $individual->xref(), 'tree' => $individual->tree()->name(), 'module' => $tab->name()])) ?>" href="#<?= $tab->name() ?>"> + <a class="nav-link<?= $tab->isGrayedOut($individual) ? ' text-muted' : '' ?>" data-toggle="tab" role="tab" data-href="<?= e(route('module', ['module' => $tab->name(), 'action' => 'Tab', 'tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) ?>" href="#<?= $tab->name() ?>"> <?= $tab->tabTitle() ?> </a> </li> @@ -117,8 +118,7 @@ use Illuminate\Support\Collection; <div class="tab-content"> <?php foreach ($tabs as $tab) : ?> <div id="<?= $tab->name() ?>" class="tab-pane fade wt-ajax-load" role="tabpanel"><?php if (!$tab->canLoadAjax()) : - ?><?= $tab->getTabContent($individual) ?><?php - endif ?></div> + ?><?= $tab->getTabContent($individual) ?><?php endif ?></div> <?php endforeach ?> </div> </div> diff --git a/resources/views/modals/create-repository.phtml b/resources/views/modals/create-repository.phtml index 093b7885df..997dc75dd7 100644 --- a/resources/views/modals/create-repository.phtml +++ b/resources/views/modals/create-repository.phtml @@ -1,6 +1,11 @@ -<?php use Fisharebest\Webtrees\I18N; ?> +<?php -<form method="post" action="<?= e(route('create-repository', ['tree' => $tree->name()])) ?>" id="wt-modal-form"> +use Fisharebest\Webtrees\Http\RequestHandlers\CreateRepositoryAction; +use Fisharebest\Webtrees\I18N; + +?> + +<form method="post" action="<?= e(route(CreateRepositoryAction::class, ['tree' => $tree->name()])) ?>" id="wt-modal-form"> <?= csrf_field() ?> <?= view('modals/header', ['title' => I18N::translate('Create a repository')]) ?> diff --git a/resources/views/modals/create-source.phtml b/resources/views/modals/create-source.phtml index 147c9fae27..90a0d9ecbe 100644 --- a/resources/views/modals/create-source.phtml +++ b/resources/views/modals/create-source.phtml @@ -1,6 +1,11 @@ -<?php use Fisharebest\Webtrees\I18N; ?> +<?php -<form method="post" action="<?= e(route('create-source', ['tree' => $tree->name()])) ?>" id="wt-modal-form"> +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSourceAction; +use Fisharebest\Webtrees\I18N; + +?> + +<form method="post" action="<?= e(route(CreateSourceAction::class, ['tree' => $tree->name()])) ?>" id="wt-modal-form"> <?= csrf_field() ?> <?= view('modals/header', ['title' => I18N::translate('Create a source')]) ?> diff --git a/resources/views/modals/create-submitter.phtml b/resources/views/modals/create-submitter.phtml index ff9f519c4f..0200ee2d1a 100644 --- a/resources/views/modals/create-submitter.phtml +++ b/resources/views/modals/create-submitter.phtml @@ -1,6 +1,11 @@ -<?php use Fisharebest\Webtrees\I18N; ?> +<?php -<form method="post" action="<?= e(route('create-submitter', ['tree' => $tree->name()])) ?>" id="wt-modal-form"> +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSubmitterAction; +use Fisharebest\Webtrees\I18N; + +?> + +<form method="post" action="<?= e(route(CreateSubmitterAction::class, ['tree' => $tree->name()])) ?>" id="wt-modal-form"> <?= csrf_field() ?> <?= view('modals/header', ['title' => I18N::translate('Create a submitter')]) ?> diff --git a/routes/web.php b/routes/web.php index ed956d4151..9b6471939a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -31,6 +31,12 @@ use Fisharebest\Webtrees\Http\Middleware\AuthMember; use Fisharebest\Webtrees\Http\Middleware\AuthModerator; use Fisharebest\Webtrees\Http\Middleware\AuthVisitor; use Fisharebest\Webtrees\Http\RequestHandlers\AddNewFact; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateRepositoryAction; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateRepositoryModal; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSourceAction; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSourceModal; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSubmitterAction; +use Fisharebest\Webtrees\Http\RequestHandlers\CreateSubmitterModal; use Fisharebest\Webtrees\Http\RequestHandlers\EditRawRecordAction; use Fisharebest\Webtrees\Http\RequestHandlers\AccountDelete; use Fisharebest\Webtrees\Http\RequestHandlers\AccountEdit; @@ -53,6 +59,7 @@ use Fisharebest\Webtrees\Http\RequestHandlers\FamilyPage; use Fisharebest\Webtrees\Http\RequestHandlers\GedcomRecordPage; use Fisharebest\Webtrees\Http\RequestHandlers\HelpText; use Fisharebest\Webtrees\Http\RequestHandlers\HomePage; +use Fisharebest\Webtrees\Http\RequestHandlers\IndividualPage; use Fisharebest\Webtrees\Http\RequestHandlers\MediaPage; use Fisharebest\Webtrees\Http\RequestHandlers\NotePage; use Fisharebest\Webtrees\Http\RequestHandlers\PasteFact; @@ -320,12 +327,12 @@ $router->attach('', '/tree/{tree}', static function (Map $router) { $router->post(CopyFact::class, '/copy/{xref}/{fact_id}', CopyFact::class); $router->get('create-note-object', '/create-note-object', 'EditNoteController::createNoteObject'); $router->post('create-note-object-action', '/create-note-object', 'EditNoteController::createNoteObjectAction'); - $router->get('create-repository', '/create-repository', 'EditRepositoryController::createRepository'); - $router->post('create-repository-action', '/create-repository', 'EditRepositoryController::createRepositoryAction'); - $router->get('create-source', '/create-source', 'EditSourceController::createSource'); - $router->post('create-source-action', '/create-source', 'EditSourceController::createSourceAction'); - $router->get('create-submitter', '/create-submitter', 'EditSubmitterController::createSubmitter'); - $router->post('create-submitter-action', '/create-submitter', 'EditSubmitterController::createSubmitterAction'); + $router->get(CreateRepositoryModal::class, '/create-repository', CreateRepositoryModal::class); + $router->post(CreateRepositoryAction::class, '/create-repository', CreateRepositoryAction::class); + $router->get(CreateSourceModal::class, '/create-source', CreateSourceModal::class); + $router->post(CreateSourceAction::class, '/create-source', CreateSourceAction::class); + $router->get(CreateSubmitterModal::class, '/create-submitter', CreateSubmitterModal::class); + $router->post(CreateSubmitterAction::class, '/create-submitter', CreateSubmitterAction::class); $router->post(DeleteRecord::class, '/delete/{xref}', DeleteRecord::class); $router->post(DeleteFact::class, '/delete/{xref}/{fact_id}', DeleteFact::class); $router->get(EditFact::class, '/edit-fact', EditFact::class); @@ -417,7 +424,7 @@ $router->attach('', '/tree/{tree}', static function (Map $router) { $router->get('contact', '/contact', 'MessageController::contactPage'); $router->post('contact-action', '/contact', 'MessageController::contactAction'); $router->get(FamilyPage::class, '/family/{xref}{/slug}', FamilyPage::class); - $router->get('individual', '/individual/{xref}{/slug}', 'IndividualController::show'); + $router->get(IndividualPage::class, '/individual/{xref}{/slug}', IndividualPage::class); $router->get('media-thumbnail', '/media-thumbnail', 'MediaFileController::mediaThumbnail'); $router->get('media-download', '/media-download', 'MediaFileController::mediaDownload'); $router->get(MediaPage::class, '/media/{xref}{/slug}', MediaPage::class); @@ -446,7 +453,6 @@ $router->attach('', '/tree/{tree}', static function (Map $router) { $router->post(Select2Submitter::class, '/select2-submitter', Select2Submitter::class); $router->post(Select2Repository::class, '/select2-repository', Select2Repository::class); $router->get(SourcePage::class, '/source/{xref}{/slug}', SourcePage::class); - $router->get('individual-tab', '/tab-{module}/{xref}', 'IndividualController::tab'); $router->get('tree-page-block', '/tree-page-block', 'HomePageController::treePageBlock'); $router->get('example', '/…'); }); |
