Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5412ccd
initial commit
simonLeary42 Dec 9, 2025
cc8190a
remove unused modal yes no buttons
simonLeary42 Dec 10, 2025
a543f93
remove modal messages
simonLeary42 Dec 10, 2025
ad53049
remove modal buttons
simonLeary42 Dec 10, 2025
eb449ee
backed enum
simonLeary42 Dec 10, 2025
adff0c8
assertMessageExists
simonLeary42 Dec 10, 2025
a2e3fb3
initialize messages
simonLeary42 Dec 10, 2025
c5e135f
redirect uses current page as default value
simonLeary42 Dec 10, 2025
cd9807f
use messages instead of modal errors
simonLeary42 Dec 10, 2025
be0674d
clear messages in test, print messages in header.php
simonLeary42 Dec 10, 2025
3f8bc5d
fix rege
simonLeary42 Dec 10, 2025
40fb59e
fix bug
simonLeary42 Dec 10, 2025
c6bebb3
move messages into main div
simonLeary42 Dec 10, 2025
a6669b6
copy django message semantics
simonLeary42 Dec 10, 2025
05ba5c6
message header is h3
simonLeary42 Dec 10, 2025
9d526e5
remove exportMessagesHTML
simonLeary42 Dec 10, 2025
03397fb
create messages.css
simonLeary42 Dec 10, 2025
7f09f76
remove success messages
simonLeary42 Dec 10, 2025
dbfb623
message CSS WIP
simonLeary42 Dec 10, 2025
a9f574e
reasonable colors
simonLeary42 Dec 10, 2025
454171c
margin, padding, color
simonLeary42 Dec 10, 2025
f4ef6dc
remove background
simonLeary42 Dec 10, 2025
0d0028a
steal colors from coldfront/bootstrap
simonLeary42 Dec 10, 2025
c5e886d
button dismiss message
simonLeary42 Dec 10, 2025
8df6e4b
steal dismiss button from coldfront
simonLeary42 Dec 10, 2025
ca5e3a3
90% width
simonLeary42 Dec 10, 2025
3692cb0
no need for extra padding
simonLeary42 Dec 10, 2025
c0146ad
fix message layout on mobile
simonLeary42 Dec 10, 2025
8820e0c
remove old comment
simonLeary42 Dec 10, 2025
93608dd
Update webroot/css/messages.css
simonLeary42 Dec 10, 2025
77509e2
htmlspecialchars
simonLeary42 Dec 10, 2025
0ea16fa
gracefully handle invalid session data
simonLeary42 Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions resources/init.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
76 changes: 75 additions & 1 deletion resources/lib/UnityHTTPD.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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 <a href='$dest'>here</a> to continue.", 302);
self::die();
Expand Down Expand Up @@ -196,4 +208,66 @@ public static function alert(string $message): void
// jsonEncode escapes quotes
echo "<script type='text/javascript'>alert(" . \jsonEncode($message) . ");</script>";
}

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"] = [];
}
}
12 changes: 12 additions & 0 deletions resources/lib/utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
28 changes: 17 additions & 11 deletions resources/templates/header.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -56,6 +56,7 @@
<link rel='stylesheet' type='text/css' href='$prefix/css/modal.css'>
<link rel='stylesheet' type='text/css' href='$prefix/css/tables.css'>
<link rel='stylesheet' type='text/css' href='$prefix/css/filters.css'>
<link rel='stylesheet' type='text/css' href='$prefix/css/messages.css'>
";
?>

Expand Down Expand Up @@ -133,23 +134,28 @@
<button style="position: absolute; right: 10px; top: 10px;" class="btnClose"></button>
</div>
<div class="modalBody"></div>
<div class="modalMessages"></div>
<div class="modalButtons">
<div class='buttonList messageButtons' style='display: none;'>
<button class='btnOkay'>Okay</button>
</div>
<div class='buttonList yesnoButtons' style='display: none;'>
<button class='btnYes'>Yes</button>
<button class='btnNo'>No</button>
</div>
</div>
</div>
</div>
<script src="<?php echo CONFIG["site"]["prefix"]; ?>/js/modal.js"></script>

<main>

<?php
foreach (UnityHTTPD::getMessages() as [$title, $body, $level]) {
echo sprintf(
"
<div class='message %s'>
<h3>%s</h3>
<p>%s</p>
<button onclick=\"this.parentElement.style.display='none';\">×</button>
</div>
",
htmlspecialchars($level->value),
htmlspecialchars($title),
htmlspecialchars($body)
);
}
UnityHTTPD::clearMessages();
if (
isset($_SESSION["is_admin"])
&& $_SESSION["is_admin"]
Expand Down
10 changes: 9 additions & 1 deletion test/functional/PIMemberRequestTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php

use PHPUnit\Framework\TestCase;
use UnityWebPortal\lib\UnityHTTPDMessageLevel;
use UnityWebPortal\lib\UnitySQL;
use UnityWebPortal\lib\UnityHTTPD;

class PIMemberRequestTest extends TestCase
{
Expand Down Expand Up @@ -43,8 +45,14 @@ public function testRequestMembership()
$this->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 {
Expand Down
29 changes: 29 additions & 0 deletions test/phpunit-bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -323,3 +326,29 @@ function getAdminUser()
{
return ["[email protected]", "foo", "bar", "[email protected]"];
}

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);
}
52 changes: 52 additions & 0 deletions webroot/css/messages.css
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 1 addition & 11 deletions webroot/css/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
3 changes: 1 addition & 2 deletions webroot/js/modal.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
Loading