18c2e8227SGreg Roach<?php 23976b470SGreg Roach 38c2e8227SGreg Roach/** 48c2e8227SGreg Roach * webtrees: online genealogy 58fcd0d32SGreg Roach * Copyright (C) 2019 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 158c2e8227SGreg Roach * along with this program. If not, see <http://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*e218f363SGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel; 240e62c4b8SGreg Roachuse Fisharebest\Webtrees\I18N; 250e62c4b8SGreg Roachuse Fisharebest\Webtrees\Individual; 260e62c4b8SGreg Roachuse Fisharebest\Webtrees\Menu; 2750d6f48cSGreg Roachuse Fisharebest\Webtrees\Services\HtmlService; 28*e218f363SGreg Roachuse Fisharebest\Webtrees\Services\TreeService; 290e62c4b8SGreg Roachuse Fisharebest\Webtrees\Tree; 304b92b602SGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 315229eadeSGreg Roachuse InvalidArgumentException; 326ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface; 336ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 34225e381fSGreg Roachuse stdClass; 3583d28054SGreg Roach 365229eadeSGreg Roachuse function assert; 37*e218f363SGreg Roachuse function redirect; 38*e218f363SGreg 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 4950d6f48cSGreg Roach /** @var HtmlService */ 5050d6f48cSGreg Roach private $html_service; 5150d6f48cSGreg Roach 52*e218f363SGreg Roach /** @var TreeService */ 53*e218f363SGreg Roach private $tree_service; 54*e218f363SGreg Roach 5550d6f48cSGreg Roach /** 56*e218f363SGreg Roach * BatchUpdateModule constructor. 5750d6f48cSGreg Roach * 5850d6f48cSGreg Roach * @param HtmlService $html_service 59*e218f363SGreg Roach * @param TreeService $tree_service 6050d6f48cSGreg Roach */ 61*e218f363SGreg Roach public function __construct(HtmlService $html_service, TreeService $tree_service) 6250d6f48cSGreg Roach { 6350d6f48cSGreg Roach $this->html_service = $html_service; 64*e218f363SGreg Roach $this->tree_service = $tree_service; 6550d6f48cSGreg Roach } 6650d6f48cSGreg Roach 6749a243cbSGreg Roach /** @var int The default access level for this module. It can be changed in the control panel. */ 6849a243cbSGreg Roach protected $access_level = Auth::PRIV_HIDE; 6949a243cbSGreg Roach 70961ec755SGreg Roach /** 71961ec755SGreg Roach * A sentence describing what this module does. 72961ec755SGreg Roach * 73961ec755SGreg Roach * @return string 74961ec755SGreg Roach */ 7549a243cbSGreg Roach public function description(): string 76c1010edaSGreg Roach { 77bbb76c12SGreg Roach /* I18N: Description of the “Stories” module */ 78bbb76c12SGreg Roach return I18N::translate('Add narrative stories to individuals in the family tree.'); 798c2e8227SGreg Roach } 808c2e8227SGreg Roach 81aee13b6dSGreg Roach /** 8249a243cbSGreg Roach * The default position for this menu. It can be changed in the control panel. 83aee13b6dSGreg Roach * 8449a243cbSGreg Roach * @return int 85aee13b6dSGreg Roach */ 8649a243cbSGreg Roach public function defaultMenuOrder(): int 87c1010edaSGreg Roach { 88353b36abSGreg Roach return 7; 898c2e8227SGreg Roach } 908c2e8227SGreg Roach 9149a243cbSGreg Roach /** 9249a243cbSGreg Roach * The default position for this tab. It can be changed in the control panel. 9349a243cbSGreg Roach * 9449a243cbSGreg Roach * @return int 9549a243cbSGreg Roach */ 96cbf4b7faSGreg Roach public function defaultTabOrder(): int 97cbf4b7faSGreg Roach { 98fb7a0427SGreg Roach return 9; 998c2e8227SGreg Roach } 1008c2e8227SGreg Roach 1013caaa4d2SGreg Roach /** 1023caaa4d2SGreg Roach * Generate the HTML content of this tab. 1033caaa4d2SGreg Roach * 1043caaa4d2SGreg Roach * @param Individual $individual 1053caaa4d2SGreg Roach * 1063caaa4d2SGreg Roach * @return string 1073caaa4d2SGreg Roach */ 1089b34404bSGreg Roach public function getTabContent(Individual $individual): string 109c1010edaSGreg Roach { 11072ac996dSGreg Roach return view('modules/stories/tab', [ 11172ac996dSGreg Roach 'is_admin' => Auth::isAdmin(), 112225e381fSGreg Roach 'individual' => $individual, 113225e381fSGreg Roach 'stories' => $this->getStoriesForIndividual($individual), 114225e381fSGreg Roach ]); 1158c2e8227SGreg Roach } 1168c2e8227SGreg Roach 117225e381fSGreg Roach /** 118225e381fSGreg Roach * @param Individual $individual 119225e381fSGreg Roach * 120225e381fSGreg Roach * @return stdClass[] 121225e381fSGreg Roach */ 122c1010edaSGreg Roach private function getStoriesForIndividual(Individual $individual): array 123c1010edaSGreg Roach { 1244b92b602SGreg Roach $block_ids = DB::table('block') 12526684e68SGreg Roach ->where('module_name', '=', $this->name()) 1264b92b602SGreg Roach ->where('xref', '=', $individual->xref()) 1274b92b602SGreg Roach ->where('gedcom_id', '=', $individual->tree()->id()) 1284b92b602SGreg Roach ->pluck('block_id'); 129225e381fSGreg Roach 130225e381fSGreg Roach $stories = []; 131225e381fSGreg Roach foreach ($block_ids as $block_id) { 1327d988ec3SGreg Roach $block_id = (int) $block_id; 1337d988ec3SGreg Roach 134225e381fSGreg Roach // Only show this block for certain languages 13550d6f48cSGreg Roach $languages = $this->getBlockSetting($block_id, 'languages'); 13622d65e5aSGreg Roach if ($languages === '' || in_array(WT_LOCALE, explode(',', $languages), true)) { 137225e381fSGreg Roach $stories[] = (object) [ 138225e381fSGreg Roach 'block_id' => $block_id, 139225e381fSGreg Roach 'title' => $this->getBlockSetting($block_id, 'title'), 14072ac996dSGreg Roach 'story_body' => $this->getBlockSetting($block_id, 'story_body'), 141225e381fSGreg Roach ]; 142225e381fSGreg Roach } 143225e381fSGreg Roach } 144225e381fSGreg Roach 145225e381fSGreg Roach return $stories; 1468c2e8227SGreg Roach } 1478c2e8227SGreg Roach 1483caaa4d2SGreg Roach /** 1493caaa4d2SGreg Roach * Is this tab empty? If so, we don't always need to display it. 1503caaa4d2SGreg Roach * 1513caaa4d2SGreg Roach * @param Individual $individual 1523caaa4d2SGreg Roach * 1533caaa4d2SGreg Roach * @return bool 1543caaa4d2SGreg Roach */ 1556ccdf4f0SGreg Roach public function hasTabContent(Individual $individual): bool 1566ccdf4f0SGreg Roach { 15754c1ab5eSGreg Roach return Auth::isManager($individual->tree()) || $this->getStoriesForIndividual($individual) !== []; 1586ccdf4f0SGreg Roach } 1596ccdf4f0SGreg Roach 1603caaa4d2SGreg Roach /** 1613caaa4d2SGreg Roach * A greyed out tab has no actual content, but may perhaps have 1623caaa4d2SGreg Roach * options to create content. 1633caaa4d2SGreg Roach * 1643caaa4d2SGreg Roach * @param Individual $individual 1653caaa4d2SGreg Roach * 1663caaa4d2SGreg Roach * @return bool 1673caaa4d2SGreg Roach */ 1686ccdf4f0SGreg Roach public function isGrayedOut(Individual $individual): bool 1696ccdf4f0SGreg Roach { 170b2448a1bSGreg Roach return $this->getStoriesForIndividual($individual) !== []; 1716ccdf4f0SGreg Roach } 1726ccdf4f0SGreg Roach 1733caaa4d2SGreg Roach /** 1743caaa4d2SGreg Roach * Can this tab load asynchronously? 1753caaa4d2SGreg Roach * 1763caaa4d2SGreg Roach * @return bool 1773caaa4d2SGreg Roach */ 1786ccdf4f0SGreg Roach public function canLoadAjax(): bool 1796ccdf4f0SGreg Roach { 1806ccdf4f0SGreg Roach return false; 1816ccdf4f0SGreg Roach } 1826ccdf4f0SGreg Roach 1838c2e8227SGreg Roach /** 1840ee13198SGreg Roach * A menu, to be added to the main application menu. 1850ee13198SGreg Roach * 186aee13b6dSGreg Roach * @param Tree $tree 187aee13b6dSGreg Roach * 1880ee13198SGreg Roach * @return Menu|null 1890ee13198SGreg Roach */ 19046295629SGreg Roach public function getMenu(Tree $tree): ?Menu 191c1010edaSGreg Roach { 19249a243cbSGreg Roach $menu = new Menu($this->title(), route('module', [ 19326684e68SGreg Roach 'module' => $this->name(), 194c1010edaSGreg Roach 'action' => 'ShowList', 1959022ab66SGreg Roach 'tree' => $tree->name(), 196c1010edaSGreg Roach ]), 'menu-story'); 1978c2e8227SGreg Roach 1988c2e8227SGreg Roach return $menu; 1998c2e8227SGreg Roach } 20072ac996dSGreg Roach 20172ac996dSGreg Roach /** 2026ccdf4f0SGreg Roach * How should this module be identified in the control panel, etc.? 2036ccdf4f0SGreg Roach * 2046ccdf4f0SGreg Roach * @return string 2056ccdf4f0SGreg Roach */ 2066ccdf4f0SGreg Roach public function title(): string 2076ccdf4f0SGreg Roach { 2086ccdf4f0SGreg Roach /* I18N: Name of a module */ 2096ccdf4f0SGreg Roach return I18N::translate('Stories'); 2106ccdf4f0SGreg Roach } 2116ccdf4f0SGreg Roach 2126ccdf4f0SGreg Roach /** 21357ab2231SGreg Roach * @param ServerRequestInterface $request 21472ac996dSGreg Roach * 2156ccdf4f0SGreg Roach * @return ResponseInterface 21672ac996dSGreg Roach */ 21757ab2231SGreg Roach public function getAdminAction(ServerRequestInterface $request): ResponseInterface 218c1010edaSGreg Roach { 21972ac996dSGreg Roach $this->layout = 'layouts/administration'; 22072ac996dSGreg Roach 221*e218f363SGreg Roach // This module can't run without a tree 22257ab2231SGreg Roach $tree = $request->getAttribute('tree'); 223*e218f363SGreg Roach 224*e218f363SGreg Roach if (!$tree instanceof Tree) { 225*e218f363SGreg Roach $tree = $this->tree_service->all()->first(); 226*e218f363SGreg Roach if ($tree instanceof Tree) { 227*e218f363SGreg Roach return redirect(route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()])); 228*e218f363SGreg Roach } 229*e218f363SGreg Roach 230*e218f363SGreg Roach return redirect(route(ControlPanel::class)); 231*e218f363SGreg Roach } 23257ab2231SGreg Roach 2334b92b602SGreg Roach $stories = DB::table('block') 23426684e68SGreg Roach ->where('module_name', '=', $this->name()) 2354b92b602SGreg Roach ->where('gedcom_id', '=', $tree->id()) 2364b92b602SGreg Roach ->orderBy('xref') 2374b92b602SGreg Roach ->get(); 23872ac996dSGreg Roach 23972ac996dSGreg Roach foreach ($stories as $story) { 2405db543e1SGreg Roach $block_id = (int) $story->block_id; 2415db543e1SGreg Roach 24272ac996dSGreg Roach $story->individual = Individual::getInstance($story->xref, $tree); 2435db543e1SGreg Roach $story->title = $this->getBlockSetting($block_id, 'title'); 2445db543e1SGreg Roach $story->languages = $this->getBlockSetting($block_id, 'languages'); 24572ac996dSGreg Roach } 24672ac996dSGreg 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, 25272ac996dSGreg Roach 'tree_names' => Tree::getNameList(), 25372ac996dSGreg Roach ]); 25472ac996dSGreg Roach } 25572ac996dSGreg Roach 25672ac996dSGreg Roach /** 2576ccdf4f0SGreg Roach * @param ServerRequestInterface $request 25872ac996dSGreg Roach * 2596ccdf4f0SGreg Roach * @return ResponseInterface 26072ac996dSGreg Roach */ 261*e218f363SGreg Roach public function postAdminAction(ServerRequestInterface $request): ResponseInterface 262*e218f363SGreg Roach { 263*e218f363SGreg Roach return redirect(route('module', [ 264*e218f363SGreg Roach 'module' => $this->name(), 265*e218f363SGreg Roach 'action' => 'Admin', 266*e218f363SGreg Roach 'tree' => $request->getParsedBody()['tree'] ?? '', 267*e218f363SGreg Roach ])); 268*e218f363SGreg Roach } 269*e218f363SGreg Roach 270*e218f363SGreg Roach /** 271*e218f363SGreg Roach * @param ServerRequestInterface $request 272*e218f363SGreg Roach * 273*e218f363SGreg Roach * @return ResponseInterface 274*e218f363SGreg Roach */ 27557ab2231SGreg Roach public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface 276c1010edaSGreg Roach { 27772ac996dSGreg Roach $this->layout = 'layouts/administration'; 27872ac996dSGreg Roach 27957ab2231SGreg Roach $tree = $request->getAttribute('tree'); 280b6b9dcc9SGreg Roach $block_id = (int) ($request->getQueryParams()['block_id'] ?? 0); 28172ac996dSGreg Roach 28272ac996dSGreg Roach if ($block_id === 0) { 28372ac996dSGreg Roach // Creating a new story 284b6b9dcc9SGreg Roach $individual = null; 28572ac996dSGreg Roach $story_title = ''; 28672ac996dSGreg Roach $story_body = ''; 28772ac996dSGreg Roach $languages = []; 28872ac996dSGreg Roach 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 29672ac996dSGreg Roach $individual = Individual::getInstance($xref, $tree); 29750d6f48cSGreg Roach $story_title = $this->getBlockSetting($block_id, 'title'); 29850d6f48cSGreg Roach $story_body = $this->getBlockSetting($block_id, 'story_body'); 29972ac996dSGreg Roach $languages = explode(',', $this->getBlockSetting($block_id, 'languages')); 30072ac996dSGreg Roach 301cc13d6d8SGreg Roach $title = I18N::translate('Edit the story') . ' — ' . e($tree->title()); 30272ac996dSGreg Roach } 30372ac996dSGreg 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, 31172ac996dSGreg Roach 'individual' => $individual, 31272ac996dSGreg Roach ]); 31372ac996dSGreg Roach } 31472ac996dSGreg Roach 31572ac996dSGreg Roach /** 3166ccdf4f0SGreg Roach * @param ServerRequestInterface $request 31772ac996dSGreg Roach * 3186ccdf4f0SGreg Roach * @return ResponseInterface 31972ac996dSGreg Roach */ 32057ab2231SGreg Roach public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface 321c1010edaSGreg Roach { 32257ab2231SGreg Roach $tree = $request->getAttribute('tree'); 323b6b9dcc9SGreg Roach $block_id = (int) ($request->getQueryParams()['block_id'] ?? 0); 324b6b9dcc9SGreg Roach 325b6b9dcc9SGreg Roach $params = $request->getParsedBody(); 326b6b9dcc9SGreg Roach 327b6b9dcc9SGreg Roach $xref = $params['xref']; 328b6b9dcc9SGreg Roach $story_body = $params['story_body']; 329b6b9dcc9SGreg Roach $story_title = $params['story_title']; 330b6b9dcc9SGreg Roach $languages = $params['languages'] ?? []; 33172ac996dSGreg Roach 33250d6f48cSGreg Roach $story_body = $this->html_service->sanitize($story_body); 33350d6f48cSGreg Roach $story_title = $this->html_service->sanitize($story_title); 33450d6f48cSGreg Roach 33572ac996dSGreg Roach if ($block_id !== 0) { 3364b92b602SGreg Roach DB::table('block') 3374b92b602SGreg Roach ->where('block_id', '=', $block_id) 3384b92b602SGreg Roach ->update([ 3394b92b602SGreg Roach 'gedcom_id' => $tree->id(), 34072ac996dSGreg Roach 'xref' => $xref, 34172ac996dSGreg Roach ]); 34272ac996dSGreg Roach } else { 3434b92b602SGreg Roach DB::table('block')->insert([ 3444b92b602SGreg Roach 'gedcom_id' => $tree->id(), 34572ac996dSGreg Roach 'xref' => $xref, 34626684e68SGreg Roach 'module_name' => $this->name(), 3474b92b602SGreg Roach 'block_order' => 0, 34872ac996dSGreg Roach ]); 34972ac996dSGreg Roach 3504b92b602SGreg Roach $block_id = (int) DB::connection()->getPdo()->lastInsertId(); 35172ac996dSGreg Roach } 35272ac996dSGreg Roach 35372ac996dSGreg Roach $this->setBlockSetting($block_id, 'story_body', $story_body); 35472ac996dSGreg Roach $this->setBlockSetting($block_id, 'title', $story_title); 35572ac996dSGreg Roach $this->setBlockSetting($block_id, 'languages', implode(',', $languages)); 35672ac996dSGreg Roach 357c1010edaSGreg Roach $url = route('module', [ 35826684e68SGreg Roach 'module' => $this->name(), 359c1010edaSGreg Roach 'action' => 'Admin', 3609022ab66SGreg Roach 'tree' => $tree->name(), 361c1010edaSGreg Roach ]); 36272ac996dSGreg Roach 3636ccdf4f0SGreg Roach return redirect($url); 36472ac996dSGreg Roach } 36572ac996dSGreg Roach 36672ac996dSGreg Roach /** 3676ccdf4f0SGreg Roach * @param ServerRequestInterface $request 36872ac996dSGreg Roach * 3696ccdf4f0SGreg Roach * @return ResponseInterface 37072ac996dSGreg Roach */ 37157ab2231SGreg Roach public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface 372c1010edaSGreg Roach { 37357ab2231SGreg Roach $tree = $request->getAttribute('tree'); 374b6b9dcc9SGreg Roach $block_id = $request->getQueryParams()['block_id']; 37572ac996dSGreg Roach 3764b92b602SGreg Roach DB::table('block_setting') 3774b92b602SGreg Roach ->where('block_id', '=', $block_id) 3784b92b602SGreg Roach ->delete(); 37972ac996dSGreg Roach 3804b92b602SGreg Roach DB::table('block') 3814b92b602SGreg Roach ->where('block_id', '=', $block_id) 3824b92b602SGreg Roach ->delete(); 38372ac996dSGreg Roach 384c1010edaSGreg Roach $url = route('module', [ 38526684e68SGreg Roach 'module' => $this->name(), 386c1010edaSGreg Roach 'action' => 'Admin', 3879022ab66SGreg Roach 'tree' => $tree->name(), 388c1010edaSGreg Roach ]); 38972ac996dSGreg Roach 3906ccdf4f0SGreg Roach return redirect($url); 39172ac996dSGreg Roach } 39272ac996dSGreg Roach 39372ac996dSGreg Roach /** 39457ab2231SGreg Roach * @param ServerRequestInterface $request 39572ac996dSGreg Roach * 3966ccdf4f0SGreg Roach * @return ResponseInterface 39772ac996dSGreg Roach */ 39857ab2231SGreg Roach public function getShowListAction(ServerRequestInterface $request): ResponseInterface 399c1010edaSGreg Roach { 40057ab2231SGreg Roach $tree = $request->getAttribute('tree'); 4015229eadeSGreg Roach assert($tree instanceof Tree, new InvalidArgumentException()); 40257ab2231SGreg Roach 4034b92b602SGreg Roach $stories = DB::table('block') 40426684e68SGreg Roach ->where('module_name', '=', $this->name()) 4054b92b602SGreg Roach ->where('gedcom_id', '=', $tree->id()) 4064b92b602SGreg Roach ->get() 4074b92b602SGreg Roach ->map(function (stdClass $story) use ($tree): stdClass { 4085db543e1SGreg Roach $block_id = (int) $story->block_id; 4095db543e1SGreg Roach 41072ac996dSGreg Roach $story->individual = Individual::getInstance($story->xref, $tree); 4115db543e1SGreg Roach $story->title = $this->getBlockSetting($block_id, 'title'); 4125db543e1SGreg Roach $story->languages = $this->getBlockSetting($block_id, 'languages'); 41372ac996dSGreg Roach 4144b92b602SGreg Roach return $story; 4150b5fd0a6SGreg Roach })->filter(static function (stdClass $story): bool { 41672ac996dSGreg Roach // Filter non-existant and private individuals. 4174b92b602SGreg Roach return $story->individual instanceof Individual && $story->individual->canShow(); 4180b5fd0a6SGreg Roach })->filter(static function (stdClass $story): bool { 41972ac996dSGreg Roach // Filter foreign languages. 42022d65e5aSGreg Roach return $story->languages === '' || in_array(WT_LOCALE, explode(',', $story->languages), true); 42172ac996dSGreg Roach }); 42272ac996dSGreg Roach 42372ac996dSGreg Roach return $this->viewResponse('modules/stories/list', [ 42472ac996dSGreg Roach 'stories' => $stories, 42549a243cbSGreg Roach 'title' => $this->title(), 42672ac996dSGreg Roach ]); 42772ac996dSGreg Roach } 4288c2e8227SGreg Roach} 429