diff --git a/composer.json b/composer.json index 401a3152a..d02ea5f46 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "ext-mbstring": "*", "ext-simplexml": "*", "mediawiki/oauthclient": "^1.2", - "php-amqplib/php-amqplib": ">=3.0" + "php-amqplib/php-amqplib": ">=3.0", + "league/commonmark": "^1.6" }, "require-dev": { "squizlabs/php_codesniffer": "^3", diff --git a/composer.lock b/composer.lock index 38c965d96..2e7fa5f00 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "518452170b275417a79fb4dce0dbf77e", + "content-hash": "32e7addf4fdc8b5d3986e244a430fbee", "packages": [ { "name": "bacon/bacon-qr-code", @@ -331,6 +331,93 @@ ], "time": "2020-06-18T20:53:17+00:00" }, + { + "name": "league/commonmark", + "version": "1.6.7", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "2b8185c13bc9578367a5bf901881d1c1b5bbd09b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/2b8185c13bc9578367a5bf901881d1c1b5bbd09b", + "reference": "2b8185c13bc9578367a5bf901881d1c1b5bbd09b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "scrutinizer/ocular": "1.7.*" + }, + "require-dev": { + "cebe/markdown": "~1.0", + "commonmark/commonmark.js": "0.29.2", + "erusev/parsedown": "~1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "~1.4", + "mikehaertl/php-shellcommand": "^1.4", + "phpstan/phpstan": "^0.12.90", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.2", + "scrutinizer/ocular": "^1.5", + "symfony/finder": "^4.2" + }, + "bin": [ + "bin/commonmark" + ], + "type": "library", + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and Github-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2022-01-13T17:18:13+00:00" + }, { "name": "mediawiki/oauthclient", "version": "1.2.0", diff --git a/includes/DataObjects/Request.php b/includes/DataObjects/Request.php index 40f840775..1cdc089cb 100644 --- a/includes/DataObjects/Request.php +++ b/includes/DataObjects/Request.php @@ -37,6 +37,7 @@ class Request extends DataObject private $forwardedip; private $hasComments = false; private $hasCommentsResolved = false; + private $originform; /** * @throws Exception @@ -50,11 +51,11 @@ public function save() INSERT INTO `request` ( email, ip, name, status, date, emailsent, emailconfirm, reserved, useragent, forwardedip, - queue + queue, originform ) VALUES ( :email, :ip, :name, :status, CURRENT_TIMESTAMP(), :emailsent, :emailconfirm, :reserved, :useragent, :forwardedip, - :queue + :queue, :originform ); SQL ); @@ -68,6 +69,7 @@ public function save() $statement->bindValue(':useragent', $this->useragent); $statement->bindValue(':forwardedip', $this->forwardedip); $statement->bindValue(':queue', $this->queue); + $statement->bindValue(':originform', $this->originform); if ($statement->execute()) { $this->id = (int)$this->dbObject->lastInsertId(); @@ -455,4 +457,32 @@ public function setQueue(?int $queue): void { $this->queue = $queue; } + + /** + * @return int|null + */ + public function getOriginForm(): ?int + { + return $this->originform; + } + + public function getOriginFormObject(): ?RequestForm + { + if ($this->originform === null) { + return null; + } + + /** @var RequestForm|bool $form */ + $form = RequestForm::getById($this->originform, $this->getDatabase()); + + return $form === false ? null : $form; + } + + /** + * @param int|null $originForm + */ + public function setOriginForm(?int $originForm): void + { + $this->originform = $originForm; + } } diff --git a/includes/DataObjects/RequestForm.php b/includes/DataObjects/RequestForm.php index 0a347c7b7..66550632a 100644 --- a/includes/DataObjects/RequestForm.php +++ b/includes/DataObjects/RequestForm.php @@ -9,8 +9,10 @@ namespace Waca\DataObjects; use Exception; +use PDO; use Waca\DataObject; use Waca\Exceptions\OptimisticLockFailedException; +use Waca\PdoDatabase; class RequestForm extends DataObject { @@ -19,13 +21,88 @@ class RequestForm extends DataObject /** @var int */ private $domain; /** @var string */ - private $name; + private $name = ''; /** @var string */ - private $publicendpoint; + private $publicendpoint = ''; /** @var string */ - private $formcontent; + private $formcontent = ''; /** @var int|null */ private $overridequeue; + /** @var string */ + private $usernamehelp; + /** @var string */ + private $emailhelp; + /** @var string */ + private $commentshelp; + + /** + * @param PdoDatabase $database + * @param int $domain + * + * @return RequestForm[] + */ + public static function getAllForms(PdoDatabase $database, int $domain) + { + $statement = $database->prepare("SELECT * FROM requestform WHERE domain = :domain;"); + $statement->execute([':domain' => $domain]); + + $resultObject = $statement->fetchAll(PDO::FETCH_CLASS, get_called_class()); + + if ($resultObject === false) { + return []; + } + + /** @var RequestQueue $t */ + foreach ($resultObject as $t) { + $t->setDatabase($database); + } + + return $resultObject; + } + + public static function getByName(PdoDatabase $database, string $name, int $domain) + { + $statement = $database->prepare(<<execute([ + ':name' => $name, + ':domain' => $domain, + ]); + + /** @var RequestForm|false $result */ + $result = $statement->fetchObject(get_called_class()); + + if ($result !== false) { + $result->setDatabase($database); + } + + return $result; + } + + public static function getByPublicEndpoint(PdoDatabase $database, string $endpoint, int $domain) + { + $statement = $database->prepare(<<execute([ + ':endpoint' => $endpoint, + ':domain' => $domain, + ]); + + /** @var RequestForm|false $result */ + $result = $statement->fetchObject(get_called_class()); + + if ($result !== false) { + $result->setDatabase($database); + } + + return $result; + } public function save() { @@ -33,9 +110,9 @@ public function save() // insert $statement = $this->dbObject->prepare(<<bindValue(":publicendpoint", $this->publicendpoint); $statement->bindValue(":formcontent", $this->formcontent); $statement->bindValue(":overridequeue", $this->overridequeue); + $statement->bindValue(":usernamehelp", $this->usernamehelp); + $statement->bindValue(":emailhelp", $this->emailhelp); + $statement->bindValue(":commentshelp", $this->commentshelp); if ($statement->execute()) { $this->id = (int)$this->dbObject->lastInsertId(); @@ -63,6 +143,9 @@ public function save() publicendpoint = :publicendpoint, formcontent = :formcontent, overridequeue = :overridequeue, + usernamehelp = :usernamehelp, + emailhelp = :emailhelp, + commentshelp = :commentshelp, updateversion = updateversion + 1 WHERE id = :id AND updateversion = :updateversion; @@ -75,6 +158,10 @@ public function save() $statement->bindValue(":publicendpoint", $this->publicendpoint); $statement->bindValue(":formcontent", $this->formcontent); $statement->bindValue(":overridequeue", $this->overridequeue); + $statement->bindValue(":usernamehelp", $this->usernamehelp); + $statement->bindValue(":emailhelp", $this->emailhelp); + $statement->bindValue(":commentshelp", $this->commentshelp); + $statement->bindValue(':id', $this->id); $statement->bindValue(':updateversion', $this->updateversion); @@ -115,6 +202,17 @@ public function getDomain(): int return $this->domain; } + public function getDomainObject(): ?Domain + { + if ($this->domain !== null) { + /** @var Domain $domain */ + $domain = Domain::getById($this->domain, $this->getDatabase()); + return $domain; + } + + return null; + } + /** * @param int $domain */ @@ -187,5 +285,51 @@ public function setOverrideQueue(?int $overrideQueue): void $this->overridequeue = $overrideQueue; } + /** + * @return string + */ + public function getUsernameHelp(): ?string + { + return $this->usernamehelp; + } + + /** + * @param string $usernamehelp + */ + public function setUsernameHelp(string $usernamehelp): void + { + $this->usernamehelp = $usernamehelp; + } + + /** + * @return string + */ + public function getEmailHelp(): ?string + { + return $this->emailhelp; + } + + /** + * @param string $emailhelp + */ + public function setEmailHelp(string $emailhelp): void + { + $this->emailhelp = $emailhelp; + } + /** + * @return string + */ + public function getCommentHelp(): ?string + { + return $this->commentshelp; + } + + /** + * @param string $commenthelp + */ + public function setCommentHelp(string $commenthelp): void + { + $this->commentshelp = $commenthelp; + } } \ No newline at end of file diff --git a/includes/Fragments/NavigationMenuAccessControl.php b/includes/Fragments/NavigationMenuAccessControl.php index 745460ad6..1ca5207dd 100644 --- a/includes/Fragments/NavigationMenuAccessControl.php +++ b/includes/Fragments/NavigationMenuAccessControl.php @@ -17,6 +17,7 @@ use Waca\Pages\PageLog; use Waca\Pages\PageMain; use Waca\Pages\PageQueueManagement; +use Waca\Pages\PageRequestFormManagement; use Waca\Pages\PageSearch; use Waca\Pages\PageSiteNotice; use Waca\Pages\PageUserManagement; @@ -81,6 +82,9 @@ protected function setupNavMenuAccess($currentUser) $this->assign('nav__canQueueMgmt', $this->getSecurityManager() ->allows(PageQueueManagement::class, RoleConfiguration::MAIN, $currentUser) === SecurityManager::ALLOWED); + $this->assign('nav__canFormMgmt', $this->getSecurityManager() + ->allows(PageRequestFormManagement::class, RoleConfiguration::MAIN, + $currentUser) === SecurityManager::ALLOWED); $this->assign('nav__canErrorLog', $this->getSecurityManager() ->allows(PageErrorLogViewer::class, RoleConfiguration::MAIN, $currentUser) === SecurityManager::ALLOWED); diff --git a/includes/Fragments/RequestData.php b/includes/Fragments/RequestData.php index 76ff53302..3bedd82a8 100644 --- a/includes/Fragments/RequestData.php +++ b/includes/Fragments/RequestData.php @@ -13,6 +13,7 @@ use Waca\DataObjects\User; use Waca\Exceptions\ApplicationLogicException; use Waca\Helpers\SearchHelpers\RequestSearchHelper; +use Waca\Pages\PageRequestFormManagement; use Waca\Pages\RequestAction\PageBreakReservation; use Waca\PdoDatabase; use Waca\Providers\Interfaces\ILocationProvider; @@ -268,6 +269,9 @@ protected function setupBasicData(Request $request, SiteConfiguration $config) $this->assign('requestQueueApiName', $queue->getApiName()); } + $this->assign('canPreviewForm', $this->barrierTest('view', User::getCurrent($this->getDatabase()), PageRequestFormManagement::class)); + $this->assign('originForm', $request->getOriginFormObject()); + $isClosed = $request->getStatus() === RequestStatus::CLOSED || $request->getStatus() === RequestStatus::JOBQUEUE; $this->assign('requestIsClosed', $isClosed); } diff --git a/includes/Fragments/TemplateOutput.php b/includes/Fragments/TemplateOutput.php index 87eb3e0c2..56e05eff0 100644 --- a/includes/Fragments/TemplateOutput.php +++ b/includes/Fragments/TemplateOutput.php @@ -92,6 +92,7 @@ final protected function setUpSmarty() $this->assign('nav__canFlaggedComments', false); $this->assign('nav__canDomainMgmt', false); $this->assign('nav__canQueueMgmt', false); + $this->assign('nav__canFormMgmt', false); $this->assign('nav__canErrorLog', false); // debug helpers diff --git a/includes/Helpers/LogHelper.php b/includes/Helpers/LogHelper.php index 4f5801723..b664d888a 100644 --- a/includes/Helpers/LogHelper.php +++ b/includes/Helpers/LogHelper.php @@ -18,6 +18,7 @@ use Waca\DataObjects\JobQueue; use Waca\DataObjects\Log; use Waca\DataObjects\Request; +use Waca\DataObjects\RequestForm; use Waca\DataObjects\RequestQueue; use Waca\DataObjects\User; use Waca\DataObjects\WelcomeTemplate; @@ -174,8 +175,10 @@ public static function getLogDescription(Log $entry) 'Hospitalised' => 'sent to the hospital', 'QueueCreated' => 'created a request queue', 'QueueEdited' => 'edited a request queue', - 'DomainCreated' => 'created a domain', - 'DomainEdited' => 'edited a domain', + 'DomainCreated' => 'created a domain', + 'DomainEdited' => 'edited a domain', + 'RequestFormCreated' => 'created a request form', + 'RequestFormEdited' => 'edited a request form', ); if (array_key_exists($entry->getAction(), $lookup)) { @@ -254,6 +257,10 @@ public static function getLogActions(PdoDatabase $database) 'DomainCreated' => 'created a domain', 'DomainEdited' => 'edited a domain', ], + "Request forms" => [ + 'RequestFormCreated' => 'created a request form', + 'RequestFormEdited' => 'edited a request form', + ], ); $databaseDrivenLogKeys = $database->query(<< 'User', 'WelcomeTemplate' => 'Welcome template', 'RequestQueue' => 'Request queue', - 'Domain' => 'Domain' + 'Domain' => 'Domain', + 'RequestForm' => 'Request form' ); } @@ -413,6 +421,18 @@ private static function getObjectDescription( $domainName = htmlentities($domain->getShortName(), ENT_COMPAT, 'UTF-8'); return "{$domainName}"; + case 'RequestForm': + /** @var RequestForm $queue */ + $queue = RequestForm::getById($objectId, $database); + + if ($queue === false) { + return "Request Form #{$objectId}"; + } + + $formName = htmlentities($queue->getName(), ENT_COMPAT, 'UTF-8'); + + return "{$formName}"; + default: return '[' . $objectType . " " . $objectId . ']'; } diff --git a/includes/Helpers/Logger.php b/includes/Helpers/Logger.php index 34777c95c..d0d951463 100644 --- a/includes/Helpers/Logger.php +++ b/includes/Helpers/Logger.php @@ -17,6 +17,7 @@ use Waca\DataObjects\JobQueue; use Waca\DataObjects\Log; use Waca\DataObjects\Request; +use Waca\DataObjects\RequestForm; use Waca\DataObjects\RequestQueue; use Waca\DataObjects\SiteNotice; use Waca\DataObjects\User; @@ -432,4 +433,15 @@ public static function domainEdited(PdoDatabase $database, Domain $domain) self::createLogEntry($database, $domain, 'DomainEdited'); } #endregion + #region Request Forms + public static function requestFormCreated(PdoDatabase $database, RequestForm $requestForm) + { + self::createLogEntry($database, $requestForm, 'RequestFormCreated'); + } + + public static function requestFormEdited(PdoDatabase $database, RequestForm $requestForm) + { + self::createLogEntry($database, $requestForm, 'RequestFormEdited'); + } + #endregion } diff --git a/includes/Helpers/MarkdownRenderingHelper.php b/includes/Helpers/MarkdownRenderingHelper.php new file mode 100644 index 000000000..6c6e12fee --- /dev/null +++ b/includes/Helpers/MarkdownRenderingHelper.php @@ -0,0 +1,49 @@ + 'escape', + 'allow_unsafe_links' => false, + 'max_nesting_level' => 10 + ]; + + private $blockRenderer; + private $inlineRenderer; + + public function __construct() + { + $blockEnvironment = Environment::createCommonMarkEnvironment(); + $blockEnvironment->addExtension(new AttributesExtension()); + $blockEnvironment->mergeConfig($this->config); + $this->blockRenderer = new MarkdownConverter($blockEnvironment); + + $inlineEnvironment = new Environment(); + $inlineEnvironment->addExtension(new AttributesExtension()); + $inlineEnvironment->addExtension(new InlinesOnlyExtension()); + $inlineEnvironment->mergeConfig($this->config); + $this->inlineRenderer = new MarkdownConverter($inlineEnvironment); + } + + public function doRender(string $content): string { + return $this->blockRenderer->convertToHtml($content); + } + + public function doRenderInline(string $content): string { + return $this->inlineRenderer->convertToHtml($content); + } + +} \ No newline at end of file diff --git a/includes/Helpers/RequestQueueHelper.php b/includes/Helpers/RequestQueueHelper.php index 0bec9d2eb..8af36f2a9 100644 --- a/includes/Helpers/RequestQueueHelper.php +++ b/includes/Helpers/RequestQueueHelper.php @@ -9,6 +9,7 @@ namespace Waca\Helpers; use Waca\DataObjects\EmailTemplate; +use Waca\DataObjects\RequestForm; use Waca\DataObjects\RequestQueue; use Waca\PdoDatabase; @@ -65,4 +66,20 @@ public function isEmailTemplateTarget(RequestQueue $queue, PdoDatabase $database return $isTarget; } + + public function isRequestFormTarget(RequestQueue $queue, PdoDatabase $database): bool + { + $isTarget = false; + $forms = RequestForm::getAllForms($database, 1); // FIXME: domains + foreach ($forms as $t) { + if ($t->isEnabled()) { + if ($t->getOverrideQueue() === $queue->getId()) { + $isTarget = true; + break; + } + } + } + + return $isTarget; + } } \ No newline at end of file diff --git a/includes/Pages/PageQueueManagement.php b/includes/Pages/PageQueueManagement.php index 3126a40bc..bb84e1e39 100644 --- a/includes/Pages/PageQueueManagement.php +++ b/includes/Pages/PageQueueManagement.php @@ -133,7 +133,7 @@ protected function edit() WebRequest::postBoolean('default'), WebRequest::postBoolean('antispoof'), WebRequest::postBoolean('titleblacklist'), - $this->helper->isEmailTemplateTarget($queue, $this->getDatabase())); + $this->helper->isEmailTemplateTarget($queue, $this->getDatabase()) || $this->helper->isRequestFormTarget($queue, $this->getDatabase())); $queue->setHeader(WebRequest::postString('header')); $queue->setDisplayName(WebRequest::postString('displayName')); @@ -192,7 +192,9 @@ protected function populateFromObject(RequestQueue $queue): void $this->assign('help', $queue->getHelp()); $this->assign('logName', $queue->getLogName()); - $isTarget = $this->helper->isEmailTemplateTarget($queue, $this->getDatabase()); - $this->assign('isTarget', $isTarget); + $isQueueTarget = $this->helper->isEmailTemplateTarget($queue, $this->getDatabase()); + $isFormTarget = $this->helper->isRequestFormTarget($queue, $this->getDatabase()); + $this->assign('isTarget', $isQueueTarget); + $this->assign('isFormTarget', $isFormTarget); } } \ No newline at end of file diff --git a/includes/Pages/PageRequestFormManagement.php b/includes/Pages/PageRequestFormManagement.php new file mode 100644 index 000000000..4e94c8610 --- /dev/null +++ b/includes/Pages/PageRequestFormManagement.php @@ -0,0 +1,313 @@ +setHtmlTitle('Request Form Management'); + + $database = $this->getDatabase(); + $domainId = Domain::getCurrent($database)->getId(); + $forms = RequestForm::getAllForms($database, $domainId); + $this->assign('forms', $forms); + + $queues = []; + foreach ($forms as $f) { + $queueId = $f->getOverrideQueue(); + if ($queueId !== null) { + if (!isset($queues[$queueId])) { + /** @var RequestQueue $queue */ + $queue = RequestQueue::getById($queueId, $this->getDatabase()); + + if ($queue->getDomain() == $domainId) { + $queues[$queueId] = $queue; + } + } + } + } + + $this->assign('queues', $queues); + + $user = User::getCurrent($database); + $this->assign('canCreate', $this->barrierTest('create', $user)); + $this->assign('canEdit', $this->barrierTest('edit', $user)); + $this->assign('canView', $this->barrierTest('view', $user)); + + $this->setTemplate('form-management/main.tpl'); + } + + protected function preview() { + $previewContent = WebRequest::getSessionContext('preview'); + + $renderer = new MarkdownRenderingHelper(); + $this->assign('renderedContent', $renderer->doRender($previewContent['main'])); + $this->assign('username', $renderer->doRenderInline($previewContent['username'])); + $this->assign('email', $renderer->doRenderInline($previewContent['email'])); + $this->assign('comment', $renderer->doRenderInline($previewContent['comment'])); + + $this->setTemplate('form-management/preview.tpl'); + } + + protected function create() + { + if (WebRequest::wasPosted()) { + $this->validateCSRFToken(); + $database = $this->getDatabase(); + $domainId = Domain::getCurrent($database)->getId(); + + $form = new RequestForm(); + + $form->setDatabase($database); + $form->setDomain($domainId); + + $this->setupObjectFromPost($form); + $form->setPublicEndpoint(WebRequest::postString('endpoint')); + + if (WebRequest::postString("preview") === "preview") { + $this->populateFromObject($form); + + WebRequest::setSessionContext('preview', [ + 'main' => $form->getFormContent(), + 'username' => $form->getUsernameHelp(), + 'email' => $form->getEmailHelp(), + 'comment' => $form->getCommentHelp(), + ]); + + $this->assign('createMode', true); + $this->setTemplate('form-management/edit.tpl'); + + return; + } + + $proceed = true; + + if (RequestForm::getByPublicEndpoint($database, $form->getPublicEndpoint(), $domainId) !== false) { + SessionAlert::error("The chosen public endpoint is already in use. Please choose another."); + $proceed = false; + } + + if (preg_match('/^[A-Za-z][a-zA-Z0-9-]*$/', $form->getPublicEndpoint()) !== 1) { + SessionAlert::error("The chosen public endpoint contains invalid characters"); + $proceed = false; + } + + if (RequestForm::getByName($database, $form->getName(), $domainId) !== false) { + SessionAlert::error("The chosen name is already in use. Please choose another."); + $proceed = false; + } + + if ($form->getOverrideQueue() !== null) { + /** @var RequestQueue|bool $queue */ + $queue = RequestQueue::getById($form->getOverrideQueue(), $database); + if ($queue === false || $queue->getDomain() !== $domainId || !$queue->isEnabled()) { + SessionAlert::error("The chosen queue does not exist or is disabled."); + $proceed = false; + } + } + + if ($proceed) { + $form->save(); + Logger::requestFormCreated($database, $form); + $this->redirect('requestFormManagement'); + } + else { + $this->populateFromObject($form); + WebRequest::setSessionContext('preview', [ + 'main' => $form->getFormContent(), + 'username' => $form->getUsernameHelp(), + 'email' => $form->getEmailHelp(), + 'comment' => $form->getCommentHelp(), + ]); + + $this->assign('createMode', true); + $this->setTemplate('form-management/edit.tpl'); + } + } + else { + $this->populateFromObject(new RequestForm()); + WebRequest::setSessionContext('preview', null); + $this->assign('hidePreview', true); + + $this->assignCSRFToken(); + $this->assign('createMode', true); + $this->setTemplate('form-management/edit.tpl'); + } + } + + protected function view() + { + $database = $this->getDatabase(); + + /** @var RequestForm $form */ + $form = RequestForm::getById(WebRequest::getInt('form'), $database); + + if ($form->getDomain() !== Domain::getCurrent($database)->getId()) { + throw new AccessDeniedException($this->getSecurityManager(), $this->getDomainAccessManager()); + } + + $this->populateFromObject($form); + + if ($form->getOverrideQueue() !== null) { + $this->assign('queueObject', RequestQueue::getById($form->getOverrideQueue(), $database)); + } + + WebRequest::setSessionContext('preview', [ + 'main' => $form->getFormContent(), + 'username' => $form->getUsernameHelp(), + 'email' => $form->getEmailHelp(), + 'comment' => $form->getCommentHelp(), + ]); + + $renderer = new MarkdownRenderingHelper(); + $this->assign('renderedContent', $renderer->doRender($form->getFormContent())); + + $this->setTemplate('form-management/view.tpl'); + } + + protected function edit() + { + $database = $this->getDatabase(); + + /** @var RequestForm $form */ + $form = RequestForm::getById(WebRequest::getInt('form'), $database); + + if ($form->getDomain() !== Domain::getCurrent($database)->getId()) { + throw new AccessDeniedException($this->getSecurityManager(), $this->getDomainAccessManager()); + } + + if (WebRequest::wasPosted()) { + $this->validateCSRFToken(); + + $this->setupObjectFromPost($form); + + if (WebRequest::postString("preview") === "preview") { + $this->populateFromObject($form); + + WebRequest::setSessionContext('preview', [ + 'main' => $form->getFormContent(), + 'username' => $form->getUsernameHelp(), + 'email' => $form->getEmailHelp(), + 'comment' => $form->getCommentHelp(), + ]); + + $this->assign('createMode', false); + $this->setTemplate('form-management/edit.tpl'); + + return; + } + + $proceed = true; + + $foundForm = RequestForm::getByName($database, $form->getName(), $form->getDomain()); + if ($foundForm !== false && $foundForm->getId() !== $form->getId()) { + SessionAlert::error("The chosen name is already in use. Please choose another."); + $proceed = false; + } + + if ($form->getOverrideQueue() !== null) { + /** @var RequestQueue $queue */ + $queue = RequestQueue::getById($form->getOverrideQueue(), $database); + if ($queue === false || $queue->getDomain() !== $form->getDomain() || !$queue->isEnabled()) { + SessionAlert::error("The chosen queue does not exist or is disabled."); + $proceed = false; + } + } + + if ($proceed) { + Logger::requestFormEdited($database, $form); + $form->save(); + $this->redirect('requestFormManagement'); + } + else { + $this->populateFromObject($form); + WebRequest::setSessionContext('preview', [ + 'main' => $form->getFormContent(), + 'username' => $form->getUsernameHelp(), + 'email' => $form->getEmailHelp(), + 'comment' => $form->getCommentHelp(), + ]); + + $this->assign('createMode', false); + $this->setTemplate('form-management/edit.tpl'); + } + } + else { + $this->populateFromObject($form); + WebRequest::setSessionContext('preview', [ + 'main' => $form->getFormContent(), + 'username' => $form->getUsernameHelp(), + 'email' => $form->getEmailHelp(), + 'comment' => $form->getCommentHelp(), + ]); + + $this->assign('createMode', false); + $this->setTemplate('form-management/edit.tpl'); + } + } + + /** + * @param RequestForm $form + */ + protected function populateFromObject(RequestForm $form): void + { + $this->assignCSRFToken(); + + $this->assign('name', $form->getName()); + $this->assign('enabled', $form->isEnabled()); + $this->assign('endpoint', $form->getPublicEndpoint()); + $this->assign('queue', $form->getOverrideQueue()); + $this->assign('content', $form->getFormContent()); + $this->assign('username', $form->getUsernameHelp()); + $this->assign('email', $form->getEmailHelp()); + $this->assign('comment', $form->getCommentHelp()); + + $this->assign('domain', $form->getDomainObject()); + + $this->assign('availableQueues', RequestQueue::getEnabledQueues($this->getDatabase())); + } + + /** + * @param RequestForm $form + * + * @return void + * @throws ApplicationLogicException + */ + protected function setupObjectFromPost(RequestForm $form): void + { + if (WebRequest::postString('content') === null + || WebRequest::postString('username') === null + || WebRequest::postString('email') === null + || WebRequest::postString('comment') === null + ) { + throw new ApplicationLogicException("Form content, username help, email help, and comment help are all required fields."); + } + + $form->setName(WebRequest::postString('name')); + $form->setEnabled(WebRequest::postBoolean('enabled')); + $form->setFormContent(WebRequest::postString('content')); + $form->setOverrideQueue(WebRequest::postInt('queue')); + $form->setUsernameHelp(WebRequest::postString('username')); + $form->setEmailHelp(WebRequest::postString('email')); + $form->setCommentHelp(WebRequest::postString('comment')); + } +} diff --git a/includes/Pages/Request/PageRequestAccount.php b/includes/Pages/Request/PageRequestAccount.php index 121c58845..492681f37 100644 --- a/includes/Pages/Request/PageRequestAccount.php +++ b/includes/Pages/Request/PageRequestAccount.php @@ -12,9 +12,12 @@ use Waca\DataObjects\Comment; use Waca\DataObjects\Domain; use Waca\DataObjects\Request; +use Waca\DataObjects\RequestForm; use Waca\DataObjects\RequestQueue; +use Waca\Exceptions\ApplicationLogicException; use Waca\Exceptions\OptimisticLockFailedException; use Waca\Helpers\BanHelper; +use Waca\Helpers\MarkdownRenderingHelper; use Waca\SessionAlert; use Waca\Tasks\PublicInterfacePageBase; use Waca\Validation\RequestValidationHelper; @@ -36,67 +39,77 @@ protected function main() { // dual mode page if (WebRequest::wasPosted()) { - $request = $this->createNewRequest(); - $comment = $this->createComment(); - - $validationErrors = $this->validateRequest($request); + $request = $this->createNewRequest(null); + $this->handleFormPost($request); + } + else { + $this->handleFormRefilling(); - if (count($validationErrors) > 0) { - foreach ($validationErrors as $validationError) { - SessionAlert::error($validationError->getErrorMessage()); - } + $this->setTemplate('request/request-form.tpl'); + } + } - // Preserve the data after an error - WebRequest::setSessionContext('accountReq', - array( - 'username' => WebRequest::postString('name'), - 'email' => WebRequest::postEmail('email'), - 'comments' => WebRequest::postString('comments'), - ) - ); + /** + * Handles dynamic request forms. + * @return void + * @throws OptimisticLockFailedException + * @throws Exception + */ + protected function dynamic() + { + $database = $this->getDatabase(); - // Validation error, bomb out early. - $this->redirect(); + $pathInfo = WebRequest::pathInfo(); + $domain = Domain::getByShortName($pathInfo[1], $database); + if ($domain === false || !$domain->isEnabled()) { + throw new ApplicationLogicException("This form is not available at this time."); + } - return; - } + $form = RequestForm::getByPublicEndpoint($database, $pathInfo[2], $domain->getId()); - // actually save the request to the database - if ($this->getSiteConfiguration()->getEmailConfirmationEnabled()) { - $this->saveAsEmailConfirmation($request, $comment); - } - else { - $this->saveWithoutEmailConfirmation($request, $comment); - } + if ($form === false || !$form->isEnabled()) { + throw new ApplicationLogicException("This form is not available at this time."); + } - $this->getRequestValidationHelper()->postSaveValidations($request); + // dual mode page + if (WebRequest::wasPosted()) { + $request = $this->createNewRequest($form); + $this->handleFormPost($request); } else { - // set the form values from the session context - $context = WebRequest::getSessionContext('accountReq'); - if ($context !== null && is_array($context)) { - $this->assign('username', $context['username']); - $this->assign('email', $context['email']); - $this->assign('comments', $context['comments']); - } + $this->handleFormRefilling(); - // Clear it for a refresh - WebRequest::setSessionContext('accountReq', null); + $renderer = new MarkdownRenderingHelper(); + $this->assign('formPreamble', $renderer->doRender($form->getFormContent())); + $this->assign('formUsernameHelp', $renderer->doRenderInline($form->getUsernameHelp())); + $this->assign('formEmailHelp', $renderer->doRenderInline($form->getEmailHelp())); + $this->assign('formCommentsHelp', $renderer->doRenderInline($form->getCommentHelp())); - $this->setTemplate('request/request-form.tpl'); + $this->setTemplate('request/request-form-dynamic.tpl'); } } /** + * @param RequestForm|null $form + * * @return Request + * @throws ApplicationLogicException */ - protected function createNewRequest() + protected function createNewRequest(?RequestForm $form): Request { $database = $this->getDatabase(); $request = new Request(); - // FIXME: domains! - $request->setQueue(RequestQueue::getDefaultQueue($database, 1)->getId()); + + if ($form === null) { + $domain = 1; + } + else { + $domain = $form->getDomain(); + $request->setOriginForm($form->getId()); + } + + $request->setQueue(RequestQueue::getDefaultQueue($database, $domain)->getId()); $request->setDatabase($database); $request->setName(trim(WebRequest::postString('name'))); @@ -231,4 +244,64 @@ protected function getRequestValidationHelper(): RequestValidationHelper return $this->validationHelper; } + + /** + * @param Request $request + * + * @return void + * @throws OptimisticLockFailedException + */ + protected function handleFormPost(Request $request): void + { + $comment = $this->createComment(); + + $validationErrors = $this->validateRequest($request); + + if (count($validationErrors) > 0) { + foreach ($validationErrors as $validationError) { + SessionAlert::error($validationError->getErrorMessage()); + } + + // Preserve the data after an error + WebRequest::setSessionContext('accountReq', + array( + 'username' => WebRequest::postString('name'), + 'email' => WebRequest::postEmail('email'), + 'comments' => WebRequest::postString('comments'), + ) + ); + + // Validation error, bomb out early. + $this->redirect(); + + return; + } + + // actually save the request to the database + if ($this->getSiteConfiguration()->getEmailConfirmationEnabled()) { + $this->saveAsEmailConfirmation($request, $comment); + } + else { + $this->saveWithoutEmailConfirmation($request, $comment); + } + + $this->getRequestValidationHelper()->postSaveValidations($request); + } + + /** + * @return void + */ + protected function handleFormRefilling(): void + { + // set the form values from the session context + $context = WebRequest::getSessionContext('accountReq'); + if ($context !== null && is_array($context)) { + $this->assign('username', $context['username']); + $this->assign('email', $context['email']); + $this->assign('comments', $context['comments']); + } + + // Clear it for a refresh + WebRequest::setSessionContext('accountReq', null); + } } \ No newline at end of file diff --git a/includes/Router/PublicRequestRouter.php b/includes/Router/PublicRequestRouter.php index c8af2b281..d2adf5b46 100644 --- a/includes/Router/PublicRequestRouter.php +++ b/includes/Router/PublicRequestRouter.php @@ -53,4 +53,15 @@ protected function getDefaultRoute() { return array(PageRequestAccount::class, 'main'); } + + public function getRouteFromPath($pathInfo): array + { + if (count($pathInfo) === 3 && $pathInfo[0] === 'r') { + // this request should be routed to the dynamic request form handler + return [PageRequestAccount::class, 'dynamic']; + } + else { + return parent::getRouteFromPath($pathInfo); + } + } } \ No newline at end of file diff --git a/includes/Router/RequestRouter.php b/includes/Router/RequestRouter.php index 69ed97591..842828b4e 100644 --- a/includes/Router/RequestRouter.php +++ b/includes/Router/RequestRouter.php @@ -21,6 +21,7 @@ use Waca\Pages\PageJobQueue; use Waca\Pages\PageListFlaggedComments; use Waca\Pages\PageQueueManagement; +use Waca\Pages\PageRequestFormManagement; use Waca\Pages\PageXffDemo; use Waca\Pages\RequestAction\PageCreateRequest; use Waca\Pages\UserAuth\Login\PageOtpLogin; @@ -206,6 +207,11 @@ class RequestRouter implements IRequestRouter 'class' => PageQueueManagement::class, 'actions' => array('create', 'edit'), ), + 'requestFormManagement' => + array( + 'class' => PageRequestFormManagement::class, + 'actions' => array('create', 'edit', 'view', 'preview'), + ), 'jobQueue' => array( 'class' => PageJobQueue::class, diff --git a/includes/Security/ContentSecurityPolicyManager.php b/includes/Security/ContentSecurityPolicyManager.php index 62611caa6..3ace15b77 100644 --- a/includes/Security/ContentSecurityPolicyManager.php +++ b/includes/Security/ContentSecurityPolicyManager.php @@ -24,7 +24,8 @@ class ContentSecurityPolicyManager 'img-src' => ['self', 'data:', 'https://upload.wikimedia.org', 'https://accounts-dev.wmflabs.org/'], 'font-src' => ['self'], 'form-action' => ['self', 'oauth'], - 'frame-ancestors' => [], + 'frame-ancestors' => ['self'], + 'frame-src' => ['self'], ]; private $nonce = null; private $reportOnly = false; diff --git a/includes/Security/RoleConfiguration.php b/includes/Security/RoleConfiguration.php index b7062445b..051c4df02 100644 --- a/includes/Security/RoleConfiguration.php +++ b/includes/Security/RoleConfiguration.php @@ -22,6 +22,7 @@ use Waca\Pages\PageLog; use Waca\Pages\PageMain; use Waca\Pages\PageQueueManagement; +use Waca\Pages\PageRequestFormManagement; use Waca\Pages\PageXffDemo; use Waca\Pages\RequestAction\PageCreateRequest; use Waca\Pages\UserAuth\PageChangePassword; @@ -227,6 +228,11 @@ class RoleConfiguration PageDomainManagement::class => array( self::MAIN => self::ACCESS_ALLOW, ), + PageRequestFormManagement::class => array( + self::MAIN => self::ACCESS_ALLOW, + 'view' => self::ACCESS_ALLOW, + 'preview' => self::ACCESS_ALLOW, + ), 'RequestCreation' => array( User::CREATION_MANUAL => self::ACCESS_ALLOW, User::CREATION_OAUTH => self::ACCESS_ALLOW, @@ -277,6 +283,10 @@ class RoleConfiguration 'edit' => self::ACCESS_ALLOW, 'create' => self::ACCESS_ALLOW, ), + PageRequestFormManagement::class => array( + 'edit' => self::ACCESS_ALLOW, + 'create' => self::ACCESS_ALLOW, + ), PageDomainManagement::class => array( 'edit' => self::ACCESS_ALLOW, ), diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php index 501112e94..a7e05e020 100644 --- a/includes/SiteConfiguration.php +++ b/includes/SiteConfiguration.php @@ -19,7 +19,7 @@ class SiteConfiguration { private $baseUrl; private $filePath; - private $schemaVersion = 40; + private $schemaVersion = 41; private $debuggingTraceEnabled; private $debuggingCssBreakpointsEnabled; private $dataClearIp = '127.0.0.1'; diff --git a/includes/Validation/RequestValidationHelper.php b/includes/Validation/RequestValidationHelper.php index 598242670..a34cb38ad 100644 --- a/includes/Validation/RequestValidationHelper.php +++ b/includes/Validation/RequestValidationHelper.php @@ -225,6 +225,9 @@ public function postSaveValidations(Request $request) // Blacklist check $this->checkTitleBlacklist($request); + // Add comment for form override + $this->formOverride($request); + $bans = $this->banHelper->getBans($request); foreach ($bans as $ban) { @@ -437,4 +440,17 @@ private function deferRequest(Request $request, RequestQueue $targetQueue, $defe $comment->setComment($deferComment); $comment->save(); } + + private function formOverride(Request $request) + { + $form = $request->getOriginFormObject(); + if($form === null || $form->getOverrideQueue() === null) { + return; + } + + /** @var RequestQueue $targetQueue */ + $targetQueue = RequestQueue::getById($form->getOverrideQueue(), $request->getDatabase()); + + $this->deferRequest($request, $targetQueue, 'Request deferred automatically due to request submission through a request form with a default queue set.'); + } } diff --git a/resources/scss/_common.scss b/resources/scss/_common.scss index 3186a2543..8a31957b4 100644 --- a/resources/scss/_common.scss +++ b/resources/scss/_common.scss @@ -97,4 +97,12 @@ $borderwidth: 1, 2, 3; } .badge-pill svg.svg-inline--fa.fa-w-16 { min-width: 1.2em; +} + +iframe.preview-frame { + width: 100%; + height: 60vh; + &.preview-frame-short { + height: 30vh; + } } \ No newline at end of file diff --git a/sql/patches/patch41-requestform-usage.sql b/sql/patches/patch41-requestform-usage.sql new file mode 100644 index 000000000..bbf2f8e13 --- /dev/null +++ b/sql/patches/patch41-requestform-usage.sql @@ -0,0 +1,72 @@ +# noinspection SqlResolveForFile + +DROP PROCEDURE IF EXISTS SCHEMA_UPGRADE_SCRIPT; +DELIMITER ';;' +CREATE PROCEDURE SCHEMA_UPGRADE_SCRIPT() BEGIN + -- ------------------------------------------------------------------------- + -- Developers - set the number of the schema patch here! + -- ------------------------------------------------------------------------- + DECLARE patchversion INT DEFAULT 41; + -- ------------------------------------------------------------------------- + -- working variables + DECLARE currentschemaversion INT DEFAULT 0; + DECLARE lastversion INT; + + -- check the schema has a version table + IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE table_name = 'schemaversion' AND table_schema = DATABASE()) THEN + SIGNAL SQLSTATE '45000' SET message_text = 'Please ensure patches are run in order! This database does not have a schemaversion table.'; + END IF; + + -- get the current version + SELECT version INTO currentschemaversion FROM schemaversion; + + -- check schema is not ahead of this patch + IF currentschemaversion >= patchversion THEN + SIGNAL SQLSTATE '45000' SET message_text = 'This patch has already been applied!'; + END IF; + + -- check schema is up-to-date + SET lastversion = patchversion - 1; + IF currentschemaversion != lastversion THEN + SET @message_text = CONCAT('Please ensure patches are run in order! This patch upgrades to version ', patchversion, ', but the database is not version ', lastversion); + SIGNAL SQLSTATE '45000' SET message_text = @message_text; + END IF; + + -- ------------------------------------------------------------------------- + -- Developers - put your upgrade statements here! + -- ------------------------------------------------------------------------- + + alter table request + add originform int(10) unsigned null, + add constraint request_requestform_id_fk + foreign key (originform) references requestform (id) ; + + alter table requestform + add usernamehelp text not null, + add emailhelp text not null, + add commentshelp text not null; + + INSERT INTO requestform (updateversion, enabled, domain, name, publicendpoint, formcontent, overridequeue, usernamehelp, emailhelp, commentshelp) VALUES (0, 1, 1, 'Default form', 'default', '## Request an account! + +We will need a few bits of information in order to create your account. However, please keep in mind that you do not need an account to read the encyclopedia or look up information - that can be done by anyone with or without an account. The first thing we need is a username, and secondly, a **valid email address that we can send your password to** (please don''t use temporary inboxes, or email aliasing, as this may cause your request to be rejected). If you want to leave any comments, feel free to do so in the comments field below. Note that if you use this form, your IP address will be recorded, and displayed to [those who review account requests](https://accounts.wmflabs.org/internal.php/statistics/users). When you are done, click the "Submit" button. + +**Please note!** +We do not have access to existing account data. If you have lost your password, please reset it using [this form](https://en.wikipedia.org/wiki/Special:PasswordReset) at wikipedia.org. If you are trying to ''take over'' an account that already exists, please use ["Changing usernames/Usurpation"](http://en.wikipedia.org/wiki/WP:CHU/U) at wikipedia.org. We cannot do either of these things for you. +{:.alert.alert-warning} + +If you have not yet done so, please review the [Username Policy](https://en.wikipedia.org/wiki/Wikipedia:Username_policy) before submitting a request.', null, 'Case sensitive, first letter is always capitalized, you do not need to use all uppercase. Note that this need not be your real name. Please make sure you don''t leave any trailing spaces or underscores on your requested username. Usernames may not consist entirely of numbers, contain the following characters: `# / | [ ] { } < > @ % :` or exceed 85 characters in length.', 'We need a valid email in order to send you your password and confirm your account request. Without it, you will not receive your password, and will be unable to log in to your account.', 'Any additional details you feel are relevant may be placed here. **Please do NOT ask for a specific password. One will be randomly created for you.**'); + commit; + + drop index requestform_publicendpoint_uindex on requestform; + + create unique index requestform_domain_publicendpoint_uindex + on requestform (domain, publicendpoint); + + + -- ------------------------------------------------------------------------- + -- finally, update the schema version to indicate success + UPDATE schemaversion SET version = patchversion; +END;; +DELIMITER ';' +CALL SCHEMA_UPGRADE_SCRIPT(); +DROP PROCEDURE IF EXISTS SCHEMA_UPGRADE_SCRIPT; \ No newline at end of file diff --git a/templates/form-management/edit.tpl b/templates/form-management/edit.tpl new file mode 100644 index 000000000..a63d2eccc --- /dev/null +++ b/templates/form-management/edit.tpl @@ -0,0 +1,173 @@ +{extends file="pagebase.tpl"} +{block name="content"} +
+
+
+

Request Form Management Create and edit request forms

+
+
+
+
+
+
+ {include file="security/csrf.tpl"} + +
+ Form metadata +
+
+
+
+ +
+
+ + The name of this form. +
+
+
+ +
+
+
+
+ + + Allow this form to be used for new requests. +
+
+
+
+
+ +
+
+
+
+ +
+
+ {if $createMode} +
+
+
{$baseurl|escape}/index.php/r/{$currentDomain->getShortName()|escape}/
+
+ +
+ {else} + {$baseurl|escape}/index.php/r/{$domain->getShortName()|escape}/{$endpoint|escape} + {/if} + + The public URL of the form. Cannot be changed after creation. Must start with a letter, and only contain letters, numbers, hyphens and underscores. +
+
+
+
+ +
+
+
+
+ +
+
+ + + Choose an alternate default queue to direct requests from this form into. +
+
+
+
+ +
+
+ Form content +

Formatting in these fields is supported using Markdown syntax.

+ +
+
+
+
+ +
+
+ + The text displayed before the request form. +
+
+
+
+
+
+
+
+ +
+
+ + The text displayed underneath the username field. +
+
+
+
+
+
+
+
+ +
+
+ + The text displayed underneath the email address fields. +
+
+
+
+
+
+
+
+ +
+
+ + The text displayed underneath the comment field. +
+
+
+
+
+ + {if !isset($hidePreview)} + +
+ Preview +
+
+ +
+
+
+ {/if} + +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+{/block} \ No newline at end of file diff --git a/templates/form-management/main.tpl b/templates/form-management/main.tpl new file mode 100644 index 000000000..0ee43e329 --- /dev/null +++ b/templates/form-management/main.tpl @@ -0,0 +1,63 @@ +{extends file="pagebase.tpl"} +{block name="content"} +
+
+
+

Request Form Management Create and edit request forms

+
+ {if $canCreate} +  Create new request form + {/if} +
+
+
+
+ +
+
+ + + + + + + + + + + + {foreach from=$forms item=form } + + + + + + + + {/foreach} + +
NamePublic endpointOverride queue
+ {$form->getName()|escape} + + {if $form->isEnabled()} + Enabled + {else} + Disabled + {/if} + + {$baseurl}/index.php/r/{$form->getDomainObject()->getShortName()|escape}/{$form->getPublicEndpoint()|escape} + + {if isset($queues[$form->getOverrideQueue()])} + {$queues[$form->getOverrideQueue()]->getHeader()|escape} + {/if} + + {if $canView} +  View + {/if} + {if $canEdit} +  Edit + {/if} +
+
+
+{/block} diff --git a/templates/form-management/preview.tpl b/templates/form-management/preview.tpl new file mode 100644 index 000000000..b8a19cd18 --- /dev/null +++ b/templates/form-management/preview.tpl @@ -0,0 +1,37 @@ +{extends file="publicbase.tpl"} +{block name="content"} +
+
+ {$renderedContent} +
+
+
+
+
+
+ + + {$username} +
+
+ + +
+
+ + + {$email} +
+
+ + + {$comment} +
+
+
Send request
+
+
+
+
+{/block} +{block name="publicfooter"}{/block} diff --git a/templates/form-management/view.tpl b/templates/form-management/view.tpl new file mode 100644 index 000000000..b5faf2c7a --- /dev/null +++ b/templates/form-management/view.tpl @@ -0,0 +1,40 @@ +{extends file="pagebase.tpl"} +{block name="content"} +
+
+
+

Request Form Management Create and edit request forms

+
+
+
+ +
+
+

+ Request form: {$name|escape} + + {if $enabled} + Enabled + {else} + Disabled + {/if} + +

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+{/block} \ No newline at end of file diff --git a/templates/navigation-menu.tpl b/templates/navigation-menu.tpl index 69510c1bd..957807547 100644 --- a/templates/navigation-menu.tpl +++ b/templates/navigation-menu.tpl @@ -23,7 +23,7 @@ {/if} - {if $nav__canBan || $nav__canEmailMgmt || $nav__canWelcomeMgmt || $nav__canSiteNoticeMgmt || $nav__canUserMgmt || $nav__canJobQueue || $nav__canFlaggedComments || $nav__canQueueMgmt || $nav__canDomainMgmt || $nav__canErrorLog} + {if $nav__canBan || $nav__canEmailMgmt || $nav__canWelcomeMgmt || $nav__canSiteNoticeMgmt || $nav__canUserMgmt || $nav__canJobQueue || $nav__canFlaggedComments || $nav__canQueueMgmt || $nav__canFormMgmt || $nav__canDomainMgmt || $nav__canErrorLog}