Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
3 changes: 3 additions & 0 deletions resources/autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
require_once __DIR__ . "/lib/UnityWebhook.php";
require_once __DIR__ . "/lib/UnityRedis.php";
require_once __DIR__ . "/lib/UnityGithub.php";
require_once __DIR__ . "/lib/utils.php";
require_once __DIR__ . "/lib/exceptions/NoDieException.php";
require_once __DIR__ . "/lib/exceptions/SSOException.php";
require_once __DIR__ . "/lib/exceptions/ArrayKeyException.php";

require_once __DIR__ . "/config.php";
require __DIR__ . "/init.php";
33 changes: 23 additions & 10 deletions resources/lib/UnitySite.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use phpseclib3\Crypt\PublicKeyLoader;
use UnityWebPortal\lib\exceptions\NoDieException;
use UnityWebPortal\lib\exceptions\ArrayKeyException;

class UnitySite
{
Expand Down Expand Up @@ -138,18 +139,30 @@ public static function shutdown()
self::internalServerError("An internal server error has occurred.", data: ["error" => $e]);
}

public static function arrayGetOrBadRequest(array $array, ...$keys)
public static function getPostData(...$keys)
{
$cursor = $array;
$keysTraversed = [];
foreach ($keys as $key) {
array_push($keysTraversed, $key);
if (!isset($cursor[$key])) {
self::badRequest("array key not found: " . json_encode($keysTraversed));
}
$cursor = $cursor[$key];
try {
return \arrayGet($_POST, ...$keys);
} catch (ArrayKeyException $e) {
self::badRequest(strval($e));
}
}

public static function getUploadedFileContents($filename, $do_delete_tmpfile_after_read = true)
{
try {
$tmpfile_path = \arrayGet($_FILES, $filename, "tmp_name");
} catch (ArrayKeyException $e) {
self::badRequest(strval($e));
}
$contents = file_get_contents($tmpfile_path);
if ($contents === false) {
throw new \Exception("Failed to read file: " . $tmpfile_path);
}
if ($do_delete_tmpfile_after_read) {
unlink($tmpfile_path);
}
return $cursor;
return $contents;
}

// in firefox, the user can disable alert/confirm/prompt after the 2nd or 3rd popup
Expand Down
7 changes: 7 additions & 0 deletions resources/lib/exceptions/ArrayKeyException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace UnityWebPortal\lib\exceptions;

class ArrayKeyException extends \Exception
{
}
21 changes: 21 additions & 0 deletions resources/lib/utils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

use UnityWebPortal\lib\exceptions\ArrayKeyException;

function arrayGet($array, ...$keys)
{
$cursor = $array;
$keysTraversed = [];
foreach ($keys as $key) {
array_push($keysTraversed, $key);
if (!isset($cursor[$key])) {
throw new ArrayKeyException(
"key not found: \$array" .
// [1, 2, "foo"] => [1][2]["foo"]
implode("", array_map(fn($x) => json_encode([$x]), $keysTraversed))
);
}
$cursor = $cursor[$key];
}
return $cursor;
}
2 changes: 1 addition & 1 deletion test/functional/SSHKeyAddTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ private function addSshKeysImport(array $keys): void {
__DIR__ . "/../../webroot/panel/account.php",
["form_type" => "addKey", "add_type" => "import"]
);
$this->assertFalse(file_exists($tmp_path));
} finally {
unlink($tmp_path);
unset($_FILES["keyfile"]);
}
}
Expand Down
2 changes: 2 additions & 0 deletions test/phpunit-bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
require_once __DIR__ . "/../resources/lib/UnityWebhook.php";
require_once __DIR__ . "/../resources/lib/UnityRedis.php";
require_once __DIR__ . "/../resources/lib/UnityGithub.php";
require_once __DIR__ . "/../resources/lib/utils.php";
require_once __DIR__ . "/../resources/lib/exceptions/NoDieException.php";
require_once __DIR__ . "/../resources/lib/exceptions/SSOException.php";
require_once __DIR__ . "/../resources/lib/exceptions/ArrayKeyException.php";

$_SERVER["HTTP_HOST"] = "phpunit"; // used for config override
require_once __DIR__ . "/../resources/config.php";
Expand Down
79 changes: 0 additions & 79 deletions test/unit/UnitySiteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use UnityWebPortal\lib\exceptions\NoDieException;
// use PHPUnit\Framework\Attributes\BackupGlobals;
// use PHPUnit\Framework\Attributes\RunTestsInSeparateProcess;

Expand Down Expand Up @@ -80,82 +79,4 @@ public function testTestValidSSHKey(bool $expected, string $key)
{
$this->assertEquals($expected, UnitySite::testValidSSHKey($key));
}

public function testArrayGetOrBadRequestReturnsValueWhenKeyExists()
{
$array = [
"a" => [
"b" => [
"c" => 123
]
]
];
$result = UnitySite::arrayGetOrBadRequest($array, "a", "b", "c");
$this->assertSame(123, $result);
}

public function testArrayGetOrBadRequestReturnsArrayWhenTraversingPartially()
{
$array = [
"foo" => [
"bar" => "baz"
]
];
$result = UnitySite::arrayGetOrBadRequest($array, "foo");
$this->assertSame(["bar" => "baz"], $result);
}

public function testArrayGetOrBadRequestThrowsOnMissingKeyFirstLevel()
{
$array = ["x" => 1];
$this->expectException(NoDieException::class);
$this->expectExceptionMessage('["y"]');
UnitySite::arrayGetOrBadRequest($array, "y");
}

public function testArrayGetOrBadRequestThrowsOnMissingKeyNested()
{
$array = ["a" => []];
$this->expectException(NoDieException::class);
// Should include both levels
$this->expectExceptionMessage('["a","b"]');
UnitySite::arrayGetOrBadRequest($array, "a", "b");
}

public function testArrayGetOrBadRequestThrowsWhenValueIsNullButKeyNotSet()
{
$array = ["a" => null];
$this->expectException(NoDieException::class);
$this->expectExceptionMessage('["a"]');
UnitySite::arrayGetOrBadRequest($array, "a");
}

public function testArrayGetOrBadRequestReturnsValueWhenValueIsFalsyButSet()
{
$array = ["a" => 0];
$result = UnitySite::arrayGetOrBadRequest($array, "a");
$this->assertSame(0, $result);
}

// I suspect that this test could have unexpected interactions with other tests.
// even with RunTestsInSeparateProcess and BackupGlobalState, http_response_code()
// still persists to the next test. header("HTTP/1.1 false") puts it back to its
// initial value, but this is a hack and does not inspire confidence.
// #[BackupGlobals(true)]
// #[RunTestsInSeparateProcess]
// public function testHeaderResponseCode()
// {
// $this->assertEquals(false, http_response_code());
// $this->assertArrayNotHasKey("SERVER_PROTOCOL", $_SERVER);
// try {
// $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1";
// UnitySite::headerResponseCode(400);
// $this->assertEquals(400, http_response_code());
// UnitySite::headerResponseCode(401);
// $this->assertEquals(401, http_response_code());
// } finally {
// unset($_SERVER["SERVER_PROTOCOL"]);
// header("HTTP/1.1 false");
// }
// }
}
63 changes: 63 additions & 0 deletions test/unit/UtilsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

use UnityWebPortal\lib\exceptions\ArrayKeyException;
use PHPUnit\Framework\TestCase;

class UtilsTest extends TestCase
{
public function testArrayGetReturnsValueWhenKeyExists()
{
$array = [
"a" => [
"b" => [
"c" => 123
]
]
];
$result = \arrayGet($array, "a", "b", "c");
$this->assertSame(123, $result);
}

public function testArrayGetReturnsArrayWhenTraversingPartially()
{
$array = [
"foo" => [
"bar" => "baz"
]
];
$result = \arrayGet($array, "foo");
$this->assertSame(["bar" => "baz"], $result);
}

public function testArrayGetThrowsOnMissingKeyFirstLevel()
{
$array = ["x" => 1];
$this->expectException(ArrayKeyException::class);
$this->expectExceptionMessage('$array["y"]');
\arrayGet($array, "y");
}

public function testArrayGetThrowsOnMissingKeyNested()
{
$array = ["a" => []];
$this->expectException(ArrayKeyException::class);
// Should include both levels
$this->expectExceptionMessage('$array["a"]["b"]');
\arrayGet($array, "a", "b");
}

public function testArrayGetThrowsWhenValueIsNullButKeyNotSet()
{
$array = ["a" => null];
$this->expectException(ArrayKeyException::class);
$this->expectExceptionMessage('$array["a"]');
\arrayGet($array, "a");
}

public function testArrayGetReturnsValueWhenValueIsFalsyButSet()
{
$array = ["a" => 0];
$result = \arrayGet($array, "a");
$this->assertSame(0, $result);
}
}
13 changes: 6 additions & 7 deletions webroot/panel/account.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,22 @@
$hasGroups = count($USER->getPIGroupGIDs()) > 0;

if ($_SERVER['REQUEST_METHOD'] == "POST") {
switch (UnitySite::arrayGetOrBadRequest($_POST, "form_type")) {
switch (UnitySite::getPostData("form_type")) {
case "addKey":
$keys = array();
switch (UnitySite::arrayGetOrBadRequest($_POST, "add_type")) {
switch (UnitySite::getPostData("add_type")) {
case "paste":
array_push($keys, UnitySite::arrayGetOrBadRequest($_POST, "key"));
array_push($keys, UnitySite::getPostData("key"));
break;
case "import":
$keyPath = UnitySite::arrayGetOrBadRequest($_FILES, "keyfile", "tmp_name");
$key = file_get_contents($keyPath);
$key = UnitySite::getUploadedFileContents("keyfile");
array_push($keys, $key);
break;
case "generate":
array_push($keys, UnitySite::arrayGetOrBadRequest($_POST, "gen_key"));
array_push($keys, UnitySite::getPostData("gen_key"));
break;
case "github":
$githubUsername = UnitySite::arrayGetOrBadRequest($_POST, "gh_user");
$githubUsername = UnitySite::getPostData("gh_user");
$githubKeys = $GITHUB->getSshPublicKeys($githubUsername);
$keys = array_merge($keys, $githubKeys);
break;
Expand Down