diff --git a/resources/init.php b/resources/init.php index 614bd458..f23f2c2d 100644 --- a/resources/init.php +++ b/resources/init.php @@ -34,6 +34,10 @@ $WEBHOOK = new UnityWebhook(); $GITHUB = new UnityGithub(); +if (!array_key_exists("messages", $_SESSION)) { + $_SESSION["messages"] = []; +} + if (isset($_SERVER["REMOTE_USER"])) { // Check if SSO is enabled on this page $SSO = UnitySSO::getSSO(); diff --git a/resources/lib/UnityHTTPD.php b/resources/lib/UnityHTTPD.php index c38a4e7b..7e09cf42 100644 --- a/resources/lib/UnityHTTPD.php +++ b/resources/lib/UnityHTTPD.php @@ -4,6 +4,16 @@ use UnityWebPortal\lib\exceptions\NoDieException; use UnityWebPortal\lib\exceptions\ArrayKeyException; +use RuntimeException; + +enum UnityHTTPDMessageLevel: string +{ + case DEBUG = "debug"; + case INFO = "info"; + case SUCCESS = "success"; + case WARNING = "warning"; + case ERROR = "error"; +} class UnityHTTPD { @@ -24,8 +34,10 @@ public static function die(mixed $x = null, bool $show_user = false): never } } - public static function redirect($dest): never + public static function redirect(?string $dest = null): never { + $dest ??= pathJoin(CONFIG["site"]["prefix"], $_SERVER["REQUEST_URI"]); + $dest = htmlspecialchars($dest); header("Location: $dest"); self::errorToUser("Redirect failed, click here to continue.", 302); self::die(); @@ -196,4 +208,66 @@ public static function alert(string $message): void // jsonEncode escapes quotes echo ""; } + + private static function ensureSessionMessagesSanity() + { + if (!isset($_SESSION)) { + throw new RuntimeException('$_SESSION is unset'); + } + if (!array_key_exists("messages", $_SESSION)) { + self::errorLog( + "invalid session messages", + 'array key "messages" does not exist for $_SESSION', + data: ['$_SESSION' => $_SESSION], + ); + $_SESSION["messages"] = []; + } + if (!is_array($_SESSION["messages"])) { + $type = gettype($_SESSION["messages"]); + self::errorLog( + "invalid session messages", + "\$_SESSION['messages'] is type '$type', not an array", + data: ['$_SESSION' => $_SESSION], + ); + $_SESSION["messages"] = []; + } + } + + public static function message(string $title, string $body, UnityHTTPDMessageLevel $level) + { + self::ensureSessionMessagesSanity(); + array_push($_SESSION["messages"], [$title, $body, $level]); + } + + public static function messageDebug(string $title, string $body) + { + return self::message($title, $body, UnityHTTPDMessageLevel::DEBUG); + } + public static function messageInfo(string $title, string $body) + { + return self::message($title, $body, UnityHTTPDMessageLevel::INFO); + } + public static function messageSuccess(string $title, string $body) + { + return self::message($title, $body, UnityHTTPDMessageLevel::SUCCESS); + } + public static function messageWarning(string $title, string $body) + { + return self::message($title, $body, UnityHTTPDMessageLevel::WARNING); + } + public static function messageError(string $title, string $body) + { + return self::message($title, $body, UnityHTTPDMessageLevel::ERROR); + } + + public static function getMessages() + { + self::ensureSessionMessagesSanity(); + return $_SESSION["messages"]; + } + + public static function clearMessages() + { + $_SESSION["messages"] = []; + } } diff --git a/resources/lib/utils.php b/resources/lib/utils.php index 6b925b36..a9a04aa8 100644 --- a/resources/lib/utils.php +++ b/resources/lib/utils.php @@ -70,3 +70,15 @@ function mbDetectEncoding(string $string, ?array $encodings = null, mixed $_ = n } return $output; } + +/* https://stackoverflow.com/a/15575293/18696276 */ +function pathJoin() +{ + $paths = []; + foreach (func_get_args() as $arg) { + if ($arg !== "") { + $paths[] = $arg; + } + } + return preg_replace("#/+#", "/", join("/", $paths)); +} diff --git a/resources/templates/header.php b/resources/templates/header.php index 23435428..2e466f85 100644 --- a/resources/templates/header.php +++ b/resources/templates/header.php @@ -16,7 +16,7 @@ // header also needs to handle POST data. So this header does the PRG redirect // for all pages. unset($_POST); // unset ensures that header must not come before POST handling - UnityHTTPD::redirect(CONFIG["site"]["prefix"] . $_SERVER['REQUEST_URI']); + UnityHTTPD::redirect(); } if (isset($SSO)) { @@ -56,6 +56,7 @@ + "; ?> @@ -133,16 +134,6 @@
-
-
- - -
@@ -150,6 +141,21 @@
+

%s

+

%s

+ + + ", + htmlspecialchars($level->value), + htmlspecialchars($title), + htmlspecialchars($body) + ); + } + UnityHTTPD::clearMessages(); if ( isset($_SESSION["is_admin"]) && $_SESSION["is_admin"] diff --git a/test/functional/PIMemberRequestTest.php b/test/functional/PIMemberRequestTest.php index cea66ae7..af37add5 100644 --- a/test/functional/PIMemberRequestTest.php +++ b/test/functional/PIMemberRequestTest.php @@ -1,7 +1,9 @@ assertTrue($SQL->requestExists($uid, $gid)); $this->cancelRequest($gid); $this->assertFalse($SQL->requestExists($uid, $gid)); + UnityHTTPD::clearMessages(); $this->requestMembership("asdlkjasldkj"); - $this->assertContains("This PI doesn't exist", $_SESSION["MODAL_ERRORS"]); + assertMessageExists( + $this, + UnityHTTPDMessageLevel::ERROR, + "/.*/", + "/^This PI doesn't exist$/", + ); $this->requestMembership($pi_group->getOwner()->getMail()); $this->assertTrue($SQL->requestExists($uid, $gid)); } finally { diff --git a/test/phpunit-bootstrap.php b/test/phpunit-bootstrap.php index 0643a7e8..7cd4273d 100644 --- a/test/phpunit-bootstrap.php +++ b/test/phpunit-bootstrap.php @@ -25,6 +25,9 @@ require_once __DIR__ . "/../resources/lib/exceptions/EncodingConversionException.php"; use UnityWebPortal\lib\UnityGroup; +use UnityWebPortal\lib\UnityHTTPD; +use UnityWebPortal\lib\UnityHTTPDMessageLevel; +use PHPUnit\Framework\TestCase; $_SERVER["HTTP_HOST"] = "phpunit"; // used for config override require_once __DIR__ . "/../resources/config.php"; @@ -323,3 +326,29 @@ function getAdminUser() { return ["user1@org1.test", "foo", "bar", "user1@org1.test"]; } + +function assertMessageExists( + TestCase $test_case, + UnityHTTPDMessageLevel $level, + string $title_regex, + string $body_regex, +) { + $messages = UnityHTTPD::getMessages(); + $error_msg = sprintf( + "message(level='%s' title_regex='%s' body_regex='%s'), not found. found messages: %s", + $level->value, + $title_regex, + $body_regex, + jsonEncode($messages), + ); + $messages_with_title = array_filter($messages, fn($x) => preg_match($title_regex, $x[0])); + $messages_with_title_and_body = array_filter( + $messages_with_title, + fn($x) => preg_match($body_regex, $x[1]), + ); + $messages_with_title_and_body_and_level = array_filter( + $messages_with_title_and_body, + fn($x) => $x[2] == $level, + ); + $test_case->assertNotEmpty($messages_with_title_and_body_and_level, $error_msg); +} diff --git a/webroot/css/messages.css b/webroot/css/messages.css new file mode 100644 index 00000000..012cfaf0 --- /dev/null +++ b/webroot/css/messages.css @@ -0,0 +1,52 @@ +.message { + border-radius: 10px; + padding: 10px 40px 10px 40px; + /* needed for button position: absolute */ + position: relative; + text-align: center; + /* width: fit-content; */ + /* subtract padding from indented width */ + width: 90% - 80px; + margin-left: auto; + margin-right: auto; + margin-bottom: 20px; +} + +.message h3 { + margin: 0; +} + +.message.debug { + color: #856404; + background-color: #fff3cd; +} + +.message.success { + color: #155724; + background-color: #d4edda; +} + +.message.info { + color: #0c5460; + background-color: #d1ecf1; +} + +.message.warning { + color: #856404; + background-color: #fff3cd; +} + +.message.error { + color: #721c24; + background-color: #f8d7da; +} + +.message button { + position: absolute; + top: 0; + right: 0; + background-color: inherit; + color: inherit; + font-size: 2rem; + border: none; +} diff --git a/webroot/css/modal.css b/webroot/css/modal.css index af0aa313..a7c2e0e5 100644 --- a/webroot/css/modal.css +++ b/webroot/css/modal.css @@ -30,17 +30,7 @@ span.modalTitle { font-size: 13pt; } -div.modalMessages { - color: var(--color-text-failure); - font-size: 11pt; -} - -div.modalMessages > * { - margin-top: 7px; - display: block; -} - -div.modalBody > * { +div.modalBody>* { margin: 0; } diff --git a/webroot/js/modal.js b/webroot/js/modal.js index d7c28b3d..4f057139 100644 --- a/webroot/js/modal.js +++ b/webroot/js/modal.js @@ -1,6 +1,5 @@ -function openModal(title, link, message = "") { +function openModal(title, link) { $("span.modalTitle").html(title); - $("div.modalMessages").html(message); $.ajax({ url: link, success: function (result) { diff --git a/webroot/panel/groups.php b/webroot/panel/groups.php index ec0f60ef..a04030d1 100644 --- a/webroot/panel/groups.php +++ b/webroot/panel/groups.php @@ -7,8 +7,6 @@ use UnityWebPortal\lib\UnityHTTPD; if ($_SERVER["REQUEST_METHOD"] == "POST") { - $modalErrors = array(); - if (isset($_POST["form_type"])) { if (isset($_POST["pi"])) { $pi_groupname = $_POST["pi"]; @@ -20,7 +18,11 @@ } $pi_account = new UnityGroup($pi_groupname, $LDAP, $SQL, $MAILER, $WEBHOOK); if (!$pi_account->exists()) { - array_push($modalErrors, "This PI doesn't exist"); + UnityHTTPD::messageError( + "Invalid Group Membership Request", + "This PI doesn't exist" + ); + UnityHTTPD::redirect(); } } @@ -31,30 +33,33 @@ } if ($pi_account->exists()) { if ($pi_account->requestExists($USER)) { - array_push($modalErrors, "You've already requested this"); + UnityHTTPD::messageError( + "Invalid Group Membership Request", + "You've already requested this" + ); + UnityHTTPD::redirect(); } if ($pi_account->memberExists($USER)) { - array_push($modalErrors, "You're already in this PI group"); + UnityHTTPD::messageError( + "Invalid Group Membership Request", + "You're already in this PI group" + ); + UnityHTTPD::redirect(); } } - if (empty($modalErrors)) { - $pi_account->newUserRequest($USER); - } + $pi_account->newUserRequest($USER); + UnityHTTPD::redirect(); break; case "removePIForm": $pi_account->removeUser($USER); + UnityHTTPD::redirect(); break; case "cancelPIForm": $pi_account->cancelGroupJoinRequest($USER); + UnityHTTPD::redirect(); break; } } - $_SESSION['MODAL_ERRORS'] = $modalErrors; -} else { - if (isset($_SESSION['MODAL_ERRORS'])) { - $modalErrors = $_SESSION['MODAL_ERRORS']; - $_SESSION['MODAL_ERRORS'] = array(); // Forget after shown - } } @@ -178,19 +183,6 @@ openModal("Add New PI", "/panel/modal/new_pi.php"); }); - 0) { - $errorHTML = ""; - foreach ($modalErrors as $error) { - $errorHTML .= "" . htmlentities($error) . ""; - } - - echo "openModal('Add New PI', '" . - CONFIG["site"]["prefix"] . "/panel/modal/new_pi.php', '" . $errorHTML . "');"; - } - ?> - // tables.js uses ajax_url to populate expandable tables var ajax_url = "/panel/ajax/get_group_members.php?gid=";