18c2e8227SGreg Roach<?php 23976b470SGreg Roach 38c2e8227SGreg Roach/** 48c2e8227SGreg Roach * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 68c2e8227SGreg Roach * This program is free software: you can redistribute it and/or modify 78c2e8227SGreg Roach * it under the terms of the GNU General Public License as published by 88c2e8227SGreg Roach * the Free Software Foundation, either version 3 of the License, or 98c2e8227SGreg Roach * (at your option) any later version. 108c2e8227SGreg Roach * This program is distributed in the hope that it will be useful, 118c2e8227SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 128c2e8227SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 138c2e8227SGreg Roach * GNU General Public License for more details. 148c2e8227SGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 168c2e8227SGreg Roach */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 2076692c8bSGreg Roachnamespace Fisharebest\Webtrees\Module; 2176692c8bSGreg Roach 220e62c4b8SGreg Roachuse Fisharebest\Webtrees\Auth; 23*6f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB; 24e218f363SGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel; 250e62c4b8SGreg Roachuse Fisharebest\Webtrees\I18N; 260e62c4b8SGreg Roachuse Fisharebest\Webtrees\Individual; 270e62c4b8SGreg Roachuse Fisharebest\Webtrees\Menu; 289f0bdfcdSGreg Roachuse Fisharebest\Webtrees\Registry; 2950d6f48cSGreg Roachuse Fisharebest\Webtrees\Services\HtmlService; 30e218f363SGreg Roachuse Fisharebest\Webtrees\Services\TreeService; 310e62c4b8SGreg Roachuse Fisharebest\Webtrees\Tree; 32b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator; 336ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface; 346ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 3583d28054SGreg Roach 3610e06497SGreg Roachuse function in_array; 37e218f363SGreg Roachuse function redirect; 38e218f363SGreg Roachuse function route; 398c2e8227SGreg Roach 408c2e8227SGreg Roach/** 418c2e8227SGreg Roach * Class StoriesModule 428c2e8227SGreg Roach */ 4337eb8894SGreg Roachclass StoriesModule extends AbstractModule implements ModuleConfigInterface, ModuleMenuInterface, ModuleTabInterface 44c1010edaSGreg Roach{ 4549a243cbSGreg Roach use ModuleTabTrait; 4649a243cbSGreg Roach use ModuleConfigTrait; 4749a243cbSGreg Roach use ModuleMenuTrait; 4849a243cbSGreg Roach 4943f2f523SGreg Roach private HtmlService $html_service; 5050d6f48cSGreg Roach 5143f2f523SGreg Roach private TreeService $tree_service; 52e218f363SGreg Roach 5350d6f48cSGreg Roach /** 5450d6f48cSGreg Roach * @param HtmlService $html_service 55e218f363SGreg Roach * @param TreeService $tree_service 5650d6f48cSGreg Roach */ 57e218f363SGreg Roach public function __construct(HtmlService $html_service, TreeService $tree_service) 5850d6f48cSGreg Roach { 5950d6f48cSGreg Roach $this->html_service = $html_service; 60e218f363SGreg Roach $this->tree_service = $tree_service; 6150d6f48cSGreg Roach } 6250d6f48cSGreg Roach 6349a243cbSGreg Roach /** @var int The default access level for this module. It can be changed in the control panel. */ 6433c746f1SGreg Roach protected int $access_level = Auth::PRIV_HIDE; 6549a243cbSGreg Roach 66961ec755SGreg Roach /** 67961ec755SGreg Roach * A sentence describing what this module does. 68961ec755SGreg Roach * 69961ec755SGreg Roach * @return string 70961ec755SGreg Roach */ 7149a243cbSGreg Roach public function description(): string 72c1010edaSGreg Roach { 73bbb76c12SGreg Roach /* I18N: Description of the “Stories” module */ 74bbb76c12SGreg Roach return I18N::translate('Add narrative stories to individuals in the family tree.'); 758c2e8227SGreg Roach } 768c2e8227SGreg Roach 77aee13b6dSGreg Roach /** 7849a243cbSGreg Roach * The default position for this menu. It can be changed in the control panel. 79aee13b6dSGreg Roach * 8049a243cbSGreg Roach * @return int 81aee13b6dSGreg Roach */ 8249a243cbSGreg Roach public function defaultMenuOrder(): int 83c1010edaSGreg Roach { 84353b36abSGreg Roach return 7; 858c2e8227SGreg Roach } 868c2e8227SGreg Roach 8749a243cbSGreg Roach /** 8849a243cbSGreg Roach * The default position for this tab. It can be changed in the control panel. 8949a243cbSGreg Roach * 9049a243cbSGreg Roach * @return int 9149a243cbSGreg Roach */ 92cbf4b7faSGreg Roach public function defaultTabOrder(): int 93cbf4b7faSGreg Roach { 94fb7a0427SGreg Roach return 9; 958c2e8227SGreg Roach } 968c2e8227SGreg Roach 973caaa4d2SGreg Roach /** 983caaa4d2SGreg Roach * Generate the HTML content of this tab. 993caaa4d2SGreg Roach * 1003caaa4d2SGreg Roach * @param Individual $individual 1013caaa4d2SGreg Roach * 1023caaa4d2SGreg Roach * @return string 1033caaa4d2SGreg Roach */ 1049b34404bSGreg Roach public function getTabContent(Individual $individual): string 105c1010edaSGreg Roach { 10672ac996dSGreg Roach return view('modules/stories/tab', [ 10772ac996dSGreg Roach 'is_admin' => Auth::isAdmin(), 108225e381fSGreg Roach 'individual' => $individual, 109225e381fSGreg Roach 'stories' => $this->getStoriesForIndividual($individual), 1100eb8cd1eSGreg Roach 'tree' => $individual->tree(), 111225e381fSGreg Roach ]); 1128c2e8227SGreg Roach } 1138c2e8227SGreg Roach 114225e381fSGreg Roach /** 115225e381fSGreg Roach * @param Individual $individual 116225e381fSGreg Roach * 117f70bcff5SGreg Roach * @return array<object> 118225e381fSGreg Roach */ 119c1010edaSGreg Roach private function getStoriesForIndividual(Individual $individual): array 120c1010edaSGreg Roach { 1214b92b602SGreg Roach $block_ids = DB::table('block') 12226684e68SGreg Roach ->where('module_name', '=', $this->name()) 1234b92b602SGreg Roach ->where('xref', '=', $individual->xref()) 1244b92b602SGreg Roach ->where('gedcom_id', '=', $individual->tree()->id()) 1254b92b602SGreg Roach ->pluck('block_id'); 126225e381fSGreg Roach 127225e381fSGreg Roach $stories = []; 128225e381fSGreg Roach foreach ($block_ids as $block_id) { 1297d988ec3SGreg Roach $block_id = (int) $block_id; 1307d988ec3SGreg Roach 131225e381fSGreg Roach // Only show this block for certain languages 13250d6f48cSGreg Roach $languages = $this->getBlockSetting($block_id, 'languages'); 13365cf5706SGreg Roach if ($languages === '' || in_array(I18N::languageTag(), explode(',', $languages), true)) { 134225e381fSGreg Roach $stories[] = (object) [ 135225e381fSGreg Roach 'block_id' => $block_id, 136225e381fSGreg Roach 'title' => $this->getBlockSetting($block_id, 'title'), 13772ac996dSGreg Roach 'story_body' => $this->getBlockSetting($block_id, 'story_body'), 138225e381fSGreg Roach ]; 139225e381fSGreg Roach } 140225e381fSGreg Roach } 141225e381fSGreg Roach 142225e381fSGreg Roach return $stories; 1438c2e8227SGreg Roach } 1448c2e8227SGreg Roach 1453caaa4d2SGreg Roach /** 1463caaa4d2SGreg Roach * Is this tab empty? If so, we don't always need to display it. 1473caaa4d2SGreg Roach * 1483caaa4d2SGreg Roach * @param Individual $individual 1493caaa4d2SGreg Roach * 1503caaa4d2SGreg Roach * @return bool 1513caaa4d2SGreg Roach */ 1526ccdf4f0SGreg Roach public function hasTabContent(Individual $individual): bool 1536ccdf4f0SGreg Roach { 15454c1ab5eSGreg Roach return Auth::isManager($individual->tree()) || $this->getStoriesForIndividual($individual) !== []; 1556ccdf4f0SGreg Roach } 1566ccdf4f0SGreg Roach 1573caaa4d2SGreg Roach /** 1583caaa4d2SGreg Roach * A greyed out tab has no actual content, but may perhaps have 1593caaa4d2SGreg Roach * options to create content. 1603caaa4d2SGreg Roach * 1613caaa4d2SGreg Roach * @param Individual $individual 1623caaa4d2SGreg Roach * 1633caaa4d2SGreg Roach * @return bool 1643caaa4d2SGreg Roach */ 1656ccdf4f0SGreg Roach public function isGrayedOut(Individual $individual): bool 1666ccdf4f0SGreg Roach { 167602c43e8SCarmen return $this->getStoriesForIndividual($individual) === []; 1686ccdf4f0SGreg Roach } 1696ccdf4f0SGreg Roach 1703caaa4d2SGreg Roach /** 1713caaa4d2SGreg Roach * Can this tab load asynchronously? 1723caaa4d2SGreg Roach * 1733caaa4d2SGreg Roach * @return bool 1743caaa4d2SGreg Roach */ 1756ccdf4f0SGreg Roach public function canLoadAjax(): bool 1766ccdf4f0SGreg Roach { 1776ccdf4f0SGreg Roach return false; 1786ccdf4f0SGreg Roach } 1796ccdf4f0SGreg Roach 1808c2e8227SGreg Roach /** 1810ee13198SGreg Roach * A menu, to be added to the main application menu. 1820ee13198SGreg Roach * 183aee13b6dSGreg Roach * @param Tree $tree 184aee13b6dSGreg Roach * 1850ee13198SGreg Roach * @return Menu|null 1860ee13198SGreg Roach */ 18746295629SGreg Roach public function getMenu(Tree $tree): ?Menu 188c1010edaSGreg Roach { 189aa6311c7SGreg Roach return new Menu($this->title(), route('module', [ 19026684e68SGreg Roach 'module' => $this->name(), 191c1010edaSGreg Roach 'action' => 'ShowList', 1929022ab66SGreg Roach 'tree' => $tree->name(), 193c1010edaSGreg Roach ]), 'menu-story'); 1948c2e8227SGreg Roach } 19572ac996dSGreg Roach 19672ac996dSGreg Roach /** 1976ccdf4f0SGreg Roach * How should this module be identified in the control panel, etc.? 1986ccdf4f0SGreg Roach * 1996ccdf4f0SGreg Roach * @return string 2006ccdf4f0SGreg Roach */ 2016ccdf4f0SGreg Roach public function title(): string 2026ccdf4f0SGreg Roach { 2036ccdf4f0SGreg Roach /* I18N: Name of a module */ 2046ccdf4f0SGreg Roach return I18N::translate('Stories'); 2056ccdf4f0SGreg Roach } 2066ccdf4f0SGreg Roach 2076ccdf4f0SGreg Roach /** 20857ab2231SGreg Roach * @param ServerRequestInterface $request 20972ac996dSGreg Roach * 2106ccdf4f0SGreg Roach * @return ResponseInterface 21172ac996dSGreg Roach */ 21257ab2231SGreg Roach public function getAdminAction(ServerRequestInterface $request): ResponseInterface 213c1010edaSGreg Roach { 21472ac996dSGreg Roach $this->layout = 'layouts/administration'; 21572ac996dSGreg Roach 216e218f363SGreg Roach // This module can't run without a tree 217b55cbc6bSGreg Roach $tree = Validator::attributes($request)->treeOptional(); 218e218f363SGreg Roach 219e218f363SGreg Roach if (!$tree instanceof Tree) { 220e218f363SGreg Roach $tree = $this->tree_service->all()->first(); 221e218f363SGreg Roach if ($tree instanceof Tree) { 222e218f363SGreg Roach return redirect(route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()])); 223e218f363SGreg Roach } 224e218f363SGreg Roach 225e218f363SGreg Roach return redirect(route(ControlPanel::class)); 226e218f363SGreg Roach } 22757ab2231SGreg Roach 2284b92b602SGreg Roach $stories = DB::table('block') 22926684e68SGreg Roach ->where('module_name', '=', $this->name()) 2304b92b602SGreg Roach ->where('gedcom_id', '=', $tree->id()) 2314b92b602SGreg Roach ->orderBy('xref') 2324b92b602SGreg Roach ->get(); 23372ac996dSGreg Roach 23472ac996dSGreg Roach foreach ($stories as $story) { 2355db543e1SGreg Roach $block_id = (int) $story->block_id; 23664d90241SGreg Roach $xref = (string) $story->xref; 2375db543e1SGreg Roach 2386b9cb339SGreg Roach $story->individual = Registry::individualFactory()->make($xref, $tree); 2395db543e1SGreg Roach $story->title = $this->getBlockSetting($block_id, 'title'); 2405db543e1SGreg Roach $story->languages = $this->getBlockSetting($block_id, 'languages'); 24172ac996dSGreg Roach } 24272ac996dSGreg Roach 2431e653452SGreg Roach $tree_names = $this->tree_service->all()->map(static function (Tree $tree): string { 2441e653452SGreg Roach return $tree->title(); 2451e653452SGreg Roach }); 2461e653452SGreg Roach 24772ac996dSGreg Roach return $this->viewResponse('modules/stories/config', [ 24871378461SGreg Roach 'module' => $this->name(), 24972ac996dSGreg Roach 'stories' => $stories, 25049a243cbSGreg Roach 'title' => $this->title() . ' — ' . $tree->title(), 25172ac996dSGreg Roach 'tree' => $tree, 2521e653452SGreg Roach 'tree_names' => $tree_names, 25372ac996dSGreg Roach ]); 25472ac996dSGreg Roach } 25572ac996dSGreg Roach 25672ac996dSGreg Roach /** 2576ccdf4f0SGreg Roach * @param ServerRequestInterface $request 25872ac996dSGreg Roach * 2596ccdf4f0SGreg Roach * @return ResponseInterface 26072ac996dSGreg Roach */ 261e218f363SGreg Roach public function postAdminAction(ServerRequestInterface $request): ResponseInterface 262e218f363SGreg Roach { 263e218f363SGreg Roach return redirect(route('module', [ 264e218f363SGreg Roach 'module' => $this->name(), 265e218f363SGreg Roach 'action' => 'Admin', 266748dbe15SGreg Roach 'tree' => Validator::parsedBody($request)->string('tree'), 267e218f363SGreg Roach ])); 268e218f363SGreg Roach } 269e218f363SGreg Roach 270e218f363SGreg Roach /** 271e218f363SGreg Roach * @param ServerRequestInterface $request 272e218f363SGreg Roach * 273e218f363SGreg Roach * @return ResponseInterface 274e218f363SGreg Roach */ 27557ab2231SGreg Roach public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface 276c1010edaSGreg Roach { 27772ac996dSGreg Roach $this->layout = 'layouts/administration'; 27872ac996dSGreg Roach 279b55cbc6bSGreg Roach $tree = Validator::attributes($request)->tree(); 2809f0bdfcdSGreg Roach $block_id = Validator::queryParams($request)->integer('block_id', 0); 2819f0bdfcdSGreg Roach $url = Validator::queryParams($request)->string('url', ''); 282c532e5bbSGreg Roach 28372ac996dSGreg Roach if ($block_id === 0) { 28472ac996dSGreg Roach // Creating a new story 28572ac996dSGreg Roach $story_title = ''; 28672ac996dSGreg Roach $story_body = ''; 28772ac996dSGreg Roach $languages = []; 288c03e33f5SGreg Roach $xref = Validator::queryParams($request)->isXref()->string('xref', ''); 289cc13d6d8SGreg Roach $title = I18N::translate('Add a story') . ' — ' . e($tree->title()); 29072ac996dSGreg Roach } else { 29172ac996dSGreg Roach // Editing an existing story 2924b92b602SGreg Roach $xref = (string) DB::table('block') 2934b92b602SGreg Roach ->where('block_id', '=', $block_id) 2944b92b602SGreg Roach ->value('xref'); 29572ac996dSGreg Roach 29650d6f48cSGreg Roach $story_title = $this->getBlockSetting($block_id, 'title'); 29750d6f48cSGreg Roach $story_body = $this->getBlockSetting($block_id, 'story_body'); 29872ac996dSGreg Roach $languages = explode(',', $this->getBlockSetting($block_id, 'languages')); 299cc13d6d8SGreg Roach $title = I18N::translate('Edit the story') . ' — ' . e($tree->title()); 30072ac996dSGreg Roach } 30172ac996dSGreg Roach 3026b9cb339SGreg Roach $individual = Registry::individualFactory()->make($xref, $tree); 303453a3d0bSGreg Roach 30472ac996dSGreg Roach return $this->viewResponse('modules/stories/edit', [ 30572ac996dSGreg Roach 'block_id' => $block_id, 30672ac996dSGreg Roach 'languages' => $languages, 30772ac996dSGreg Roach 'story_body' => $story_body, 30872ac996dSGreg Roach 'story_title' => $story_title, 30972ac996dSGreg Roach 'title' => $title, 31072ac996dSGreg Roach 'tree' => $tree, 311c532e5bbSGreg Roach 'url' => $url, 31272ac996dSGreg Roach 'individual' => $individual, 31372ac996dSGreg Roach ]); 31472ac996dSGreg Roach } 31572ac996dSGreg Roach 31672ac996dSGreg Roach /** 3176ccdf4f0SGreg Roach * @param ServerRequestInterface $request 31872ac996dSGreg Roach * 3196ccdf4f0SGreg Roach * @return ResponseInterface 32072ac996dSGreg Roach */ 32157ab2231SGreg Roach public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface 322c1010edaSGreg Roach { 323b55cbc6bSGreg Roach $tree = Validator::attributes($request)->tree(); 3249f0bdfcdSGreg Roach $block_id = Validator::queryParams($request)->integer('block_id', 0); 3259f0bdfcdSGreg Roach $xref = Validator::parsedBody($request)->string('xref'); 3269f0bdfcdSGreg Roach $story_body = Validator::parsedBody($request)->string('story_body'); 3279f0bdfcdSGreg Roach $story_title = Validator::parsedBody($request)->string('story_title'); 3289f0bdfcdSGreg Roach $languages = Validator::parsedBody($request)->array('languages'); 3299f0bdfcdSGreg Roach $default_url = route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()]); 3309f0bdfcdSGreg Roach $url = Validator::parsedBody($request)->isLocalUrl()->string('url', $default_url); 33150d6f48cSGreg Roach $story_body = $this->html_service->sanitize($story_body); 33250d6f48cSGreg Roach 33372ac996dSGreg Roach if ($block_id !== 0) { 3344b92b602SGreg Roach DB::table('block') 3354b92b602SGreg Roach ->where('block_id', '=', $block_id) 3364b92b602SGreg Roach ->update([ 3374b92b602SGreg Roach 'gedcom_id' => $tree->id(), 33872ac996dSGreg Roach 'xref' => $xref, 33972ac996dSGreg Roach ]); 34072ac996dSGreg Roach } else { 3414b92b602SGreg Roach DB::table('block')->insert([ 3424b92b602SGreg Roach 'gedcom_id' => $tree->id(), 34372ac996dSGreg Roach 'xref' => $xref, 34426684e68SGreg Roach 'module_name' => $this->name(), 3454b92b602SGreg Roach 'block_order' => 0, 34672ac996dSGreg Roach ]); 34772ac996dSGreg Roach 3484b92b602SGreg Roach $block_id = (int) DB::connection()->getPdo()->lastInsertId(); 34972ac996dSGreg Roach } 35072ac996dSGreg Roach 35172ac996dSGreg Roach $this->setBlockSetting($block_id, 'story_body', $story_body); 35272ac996dSGreg Roach $this->setBlockSetting($block_id, 'title', $story_title); 35372ac996dSGreg Roach $this->setBlockSetting($block_id, 'languages', implode(',', $languages)); 35472ac996dSGreg Roach 3556ccdf4f0SGreg Roach return redirect($url); 35672ac996dSGreg Roach } 35772ac996dSGreg Roach 35872ac996dSGreg Roach /** 3596ccdf4f0SGreg Roach * @param ServerRequestInterface $request 36072ac996dSGreg Roach * 3616ccdf4f0SGreg Roach * @return ResponseInterface 36272ac996dSGreg Roach */ 36357ab2231SGreg Roach public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface 364c1010edaSGreg Roach { 365b55cbc6bSGreg Roach $tree = Validator::attributes($request)->tree(); 366748dbe15SGreg Roach $block_id = Validator::queryParams($request)->integer('block_id'); 36772ac996dSGreg Roach 3684b92b602SGreg Roach DB::table('block_setting') 3694b92b602SGreg Roach ->where('block_id', '=', $block_id) 3704b92b602SGreg Roach ->delete(); 37172ac996dSGreg Roach 3724b92b602SGreg Roach DB::table('block') 3734b92b602SGreg Roach ->where('block_id', '=', $block_id) 3744b92b602SGreg Roach ->delete(); 37572ac996dSGreg Roach 376c1010edaSGreg Roach $url = route('module', [ 37726684e68SGreg Roach 'module' => $this->name(), 378c1010edaSGreg Roach 'action' => 'Admin', 3799022ab66SGreg Roach 'tree' => $tree->name(), 380c1010edaSGreg Roach ]); 38172ac996dSGreg Roach 3826ccdf4f0SGreg Roach return redirect($url); 38372ac996dSGreg Roach } 38472ac996dSGreg Roach 38572ac996dSGreg Roach /** 38657ab2231SGreg Roach * @param ServerRequestInterface $request 38772ac996dSGreg Roach * 3886ccdf4f0SGreg Roach * @return ResponseInterface 38972ac996dSGreg Roach */ 39057ab2231SGreg Roach public function getShowListAction(ServerRequestInterface $request): ResponseInterface 391c1010edaSGreg Roach { 392b55cbc6bSGreg Roach $tree = Validator::attributes($request)->tree(); 39357ab2231SGreg Roach 3944b92b602SGreg Roach $stories = DB::table('block') 39526684e68SGreg Roach ->where('module_name', '=', $this->name()) 3964b92b602SGreg Roach ->where('gedcom_id', '=', $tree->id()) 3974b92b602SGreg Roach ->get() 398f70bcff5SGreg Roach ->map(function (object $story) use ($tree): object { 3995db543e1SGreg Roach $block_id = (int) $story->block_id; 40064d90241SGreg Roach $xref = (string) $story->xref; 4015db543e1SGreg Roach 4026b9cb339SGreg Roach $story->individual = Registry::individualFactory()->make($xref, $tree); 4035db543e1SGreg Roach $story->title = $this->getBlockSetting($block_id, 'title'); 4045db543e1SGreg Roach $story->languages = $this->getBlockSetting($block_id, 'languages'); 40572ac996dSGreg Roach 4064b92b602SGreg Roach return $story; 407f70bcff5SGreg Roach })->filter(static function (object $story): bool { 408fceda430SGreg Roach // Filter non-existent and private individuals. 4094b92b602SGreg Roach return $story->individual instanceof Individual && $story->individual->canShow(); 410f70bcff5SGreg Roach })->filter(static function (object $story): bool { 41172ac996dSGreg Roach // Filter foreign languages. 41265cf5706SGreg Roach return $story->languages === '' || in_array(I18N::languageTag(), explode(',', $story->languages), true); 41372ac996dSGreg Roach }); 41472ac996dSGreg Roach 41572ac996dSGreg Roach return $this->viewResponse('modules/stories/list', [ 41672ac996dSGreg Roach 'stories' => $stories, 41749a243cbSGreg Roach 'title' => $this->title(), 418745968f0SGreg Roach 'tree' => $tree, 41972ac996dSGreg Roach ]); 42072ac996dSGreg Roach } 4218c2e8227SGreg Roach} 422