. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Module; use Aura\Router\RouterContainer; use Closure; use Fig\Http\Message\RequestMethodInterface; use Fisharebest\Algorithm\Dijkstra; use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Contracts\UserInterface; use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\FlashMessages; use Fisharebest\Webtrees\Functions\Functions; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Menu; use Fisharebest\Webtrees\Services\TreeService; use Fisharebest\Webtrees\Tree; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Query\JoinClause; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use function app; use function assert; use function is_string; use function redirect; use function route; use function view; /** * Class RelationshipsChartModule */ class RelationshipsChartModule extends AbstractModule implements ModuleChartInterface, ModuleConfigInterface, RequestHandlerInterface { use ModuleChartTrait; use ModuleConfigTrait; protected const ROUTE_URL = '/tree/{tree}/relationships-{ancestors}-{recursion}/{xref}{/xref2}'; /** It would be more correct to use PHP_INT_MAX, but this isn't friendly in URLs */ public const UNLIMITED_RECURSION = 99; /** By default new trees allow unlimited recursion */ public const DEFAULT_RECURSION = '99'; /** By default new trees search for all relationships (not via ancestors) */ public const DEFAULT_ANCESTORS = '0'; public const DEFAULT_PARAMETERS = [ 'ancestors' => self::DEFAULT_ANCESTORS, 'recursion' => self::DEFAULT_RECURSION, ]; /** @var TreeService */ private $tree_service; /** * @param TreeService $tree_service */ public function __construct(TreeService $tree_service) { $this->tree_service = $tree_service; } /** * Initialization. * * @return void */ public function boot(): void { $router_container = app(RouterContainer::class); assert($router_container instanceof RouterContainer); $router_container->getMap() ->get(static::class, static::ROUTE_URL, $this) ->allows(RequestMethodInterface::METHOD_POST) ->tokens([ 'ancestors' => '\d+', 'recursion' => '\d+', ]); } /** * A sentence describing what this module does. * * @return string */ public function description(): string { /* I18N: Description of the “RelationshipsChart” module */ return I18N::translate('A chart displaying relationships between two individuals.'); } /** * Return a menu item for this chart - for use in individual boxes. * * @param Individual $individual * * @return Menu|null */ public function chartBoxMenu(Individual $individual): ?Menu { return $this->chartMenu($individual); } /** * A main menu item for this chart. * * @param Individual $individual * * @return Menu */ public function chartMenu(Individual $individual): Menu { $my_xref = $individual->tree()->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF); if ($my_xref !== '' && $my_xref !== $individual->xref()) { $my_record = Registry::individualFactory()->make($my_xref, $individual->tree()); return new Menu( I18N::translate('Relationship to me'), $this->chartUrl($my_record, ['xref2' => $individual->xref()]), $this->chartMenuClass(), $this->chartUrlAttributes() ); } return new Menu( $this->title(), $this->chartUrl($individual), $this->chartMenuClass(), $this->chartUrlAttributes() ); } /** * CSS class for the URL. * * @return string */ public function chartMenuClass(): string { return 'menu-chart-relationship'; } /** * How should this module be identified in the control panel, etc.? * * @return string */ public function title(): string { /* I18N: Name of a module/chart */ return I18N::translate('Relationships'); } /** * The URL for a page showing chart options. * * @param Individual $individual * @param mixed[] $parameters * * @return string */ public function chartUrl(Individual $individual, array $parameters = []): string { return route(static::class, [ 'xref' => $individual->xref(), 'tree' => $individual->tree()->name(), ] + $parameters + self::DEFAULT_PARAMETERS); } /** * @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)); $xref2 = $request->getAttribute('xref2') ?? ''; $ajax = $request->getQueryParams()['ajax'] ?? ''; $ancestors = (int) $request->getAttribute('ancestors'); $recursion = (int) $request->getAttribute('recursion'); $user = $request->getAttribute('user'); // Convert POST requests into GET requests for pretty URLs. if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { $params = (array) $request->getParsedBody(); return redirect(route(static::class, [ 'ancestors' => $params['ancestors'], 'recursion' => $params['recursion'], 'tree' => $tree->name(), 'xref' => $params['xref'], 'xref2' => $params['xref2'], ])); } $individual1 = Registry::individualFactory()->make($xref, $tree); $individual2 = Registry::individualFactory()->make($xref2, $tree); $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', static::DEFAULT_ANCESTORS); $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); $recursion = min($recursion, $max_recursion); if ($individual1 instanceof Individual) { $individual1 = Auth::checkIndividualAccess($individual1, false, true); } if ($individual2 instanceof Individual) { $individual2 = Auth::checkIndividualAccess($individual2, false, true); } Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user); if ($individual1 instanceof Individual && $individual2 instanceof Individual) { if ($ajax === '1') { return $this->chart($individual1, $individual2, $recursion, $ancestors); } /* I18N: %s are individual’s names */ $title = I18N::translate('Relationships between %1$s and %2$s', $individual1->fullName(), $individual2->fullName()); $ajax_url = $this->chartUrl($individual1, [ 'ajax' => true, 'ancestors' => $ancestors, 'recursion' => $recursion, 'xref2' => $individual2->xref(), ]); } else { $title = I18N::translate('Relationships'); $ajax_url = ''; } return $this->viewResponse('modules/relationships-chart/page', [ 'ajax_url' => $ajax_url, 'ancestors' => $ancestors, 'ancestors_only' => $ancestors_only, 'ancestors_options' => $this->ancestorsOptions(), 'individual1' => $individual1, 'individual2' => $individual2, 'max_recursion' => $max_recursion, 'module' => $this->name(), 'recursion' => $recursion, 'recursion_options' => $this->recursionOptions($max_recursion), 'title' => $title, 'tree' => $tree, ]); } /** * @param Individual $individual1 * @param Individual $individual2 * @param int $recursion * @param int $ancestors * * @return ResponseInterface */ public function chart(Individual $individual1, Individual $individual2, int $recursion, int $ancestors): ResponseInterface { $tree = $individual1->tree(); $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); $recursion = min($recursion, $max_recursion); $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors); ob_start(); if (I18N::direction() === 'ltr') { $diagonal1 = asset('css/images/dline.png'); $diagonal2 = asset('css/images/dline2.png'); } else { $diagonal1 = asset('css/images/dline2.png'); $diagonal2 = asset('css/images/dline.png'); } $num_paths = 0; foreach ($paths as $path) { // Extract the relationship names between pairs of individuals $relationships = $this->oldStyleRelationshipPath($tree, $path); if ($relationships === []) { // Cannot see one of the families/individuals, due to privacy; continue; } echo '
'; if (isset($table[$x][$y])) { echo $table[$x][$y]; } echo ' | '; } echo '
', I18N::translate('No link between the two individuals could be found.'), '
'; } $html = ob_get_clean(); return response($html); } /** * @param ServerRequestInterface $request * * @return ResponseInterface */ public function getAdminAction(ServerRequestInterface $request): ResponseInterface { $this->layout = 'layouts/administration'; return $this->viewResponse('modules/relationships-chart/config', [ 'all_trees' => $this->tree_service->all(), 'ancestors_options' => $this->ancestorsOptions(), 'default_ancestors' => self::DEFAULT_ANCESTORS, 'default_recursion' => self::DEFAULT_RECURSION, 'recursion_options' => $this->recursionConfigOptions(), 'title' => I18N::translate('Chart preferences') . ' — ' . $this->title(), ]); } /** * @param ServerRequestInterface $request * * @return ResponseInterface */ public function postAdminAction(ServerRequestInterface $request): ResponseInterface { $params = (array) $request->getParsedBody(); foreach ($this->tree_service->all() as $tree) { $recursion = $params['relationship-recursion-' . $tree->id()] ?? ''; $ancestors = $params['relationship-ancestors-' . $tree->id()] ?? ''; $tree->setPreference('RELATIONSHIP_RECURSION', $recursion); $tree->setPreference('RELATIONSHIP_ANCESTORS', $ancestors); } FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success'); return redirect($this->getConfigLink()); } /** * Possible options for the ancestors option * * @return array