Skip to content

Commit

Permalink
Merge branch 'master' into form-param
Browse files Browse the repository at this point in the history
  • Loading branch information
notbakaneko authored Nov 21, 2024
2 parents ca8ce96 + cd26bb5 commit 693fd4a
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 26 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ CLIENT_CHECK_VERSION=false
# USER_PROFILE_SCORES_NOTICE=

# MULTIPLAYER_MAX_ATTEMPTS_LIMIT=128
# MULTIPLAYER_ROOM_CLOSE_GRACE_PERIOD_MINUTES=5

# NOTIFICATION_QUEUE=notification
# NOTIFICATION_REDIS_HOST=127.0.0.1
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,16 @@ jobs:
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
restore-keys: ${{ runner.os }}-composer-

- name: Get current date
id: current-date
run: echo "today=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"

- name: Cache ip2asn
uses: actions/cache@v4
with:
path: database/ip2asn/
key: ip2asn
key: ip2asn-${{ steps.current-date.outputs.today }}
restore-keys: ip2asn-

- run: ./build.sh

Expand Down
7 changes: 7 additions & 0 deletions app/Http/Controllers/Multiplayer/RoomsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ public function __construct()
$this->middleware('require-scopes:public', ['only' => ['index', 'leaderboard', 'show']]);
}

public function destroy($id)
{
Room::findOrFail($id)->endGame(\Auth::user());

return response(null, 204);
}

/**
* Get Multiplayer Rooms
*
Expand Down
4 changes: 2 additions & 2 deletions app/Http/Controllers/RankingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ public function index($mode, $type)

$maxResults = $this->maxResults($modeInt, $stats);
$maxPages = ceil($maxResults / static::PAGE_SIZE);
// TODO: less repeatedly getting params out of request.
$page = \Number::clamp(get_int(request('cursor.page') ?? request('page') ?? 1), 1, $maxPages);
$params = get_params(\Request::all(), null, ['cursor.page:int', 'page:int']);
$page = \Number::clamp($params['cursor']['page'] ?? $params['page'] ?? 1, 1, $maxPages);

$stats = $stats->limit(static::PAGE_SIZE)
->offset(static::PAGE_SIZE * ($page - 1))
Expand Down
24 changes: 22 additions & 2 deletions app/Models/Multiplayer/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ public function calculateMissingTopScores()

public function completePlay(ScoreToken $scoreToken, array $params): ScoreLink
{
priv_check_user($scoreToken->user, 'MultiplayerScoreSubmit')->ensureCan();
priv_check_user($scoreToken->user, 'MultiplayerScoreSubmit', $this)->ensureCan();

$this->assertValidCompletePlay();

Expand Down Expand Up @@ -622,9 +622,29 @@ public function startGame(User $host, array $rawParams, array $extraParams = [])
return $this->fresh();
}

/**
* @throws InvariantException
*/
public function endGame(User $requestingUser)
{
priv_check_user($requestingUser, 'MultiplayerRoomDestroy', $this)->ensureCan();

if ($this->isRealtime()) {
throw new InvariantException('Realtime rooms cannot be closed.');
}

$gracePeriodMinutes = $GLOBALS['cfg']['osu']['multiplayer']['room_close_grace_period_minutes'];
if ($this->starts_at->addMinutes($gracePeriodMinutes)->isPast()) {
throw new InvariantException('The grace period for closing this room has expired.');
}

$this->ends_at = now();
$this->save();
}

public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId)
{
priv_check_user($user, 'MultiplayerScoreSubmit')->ensureCan();
priv_check_user($user, 'MultiplayerScoreSubmit', $this)->ensureCan();

$this->assertValidStartPlay($user, $playlistItem);

Expand Down
53 changes: 33 additions & 20 deletions app/Singletons/OsuAuthorize.php
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,7 @@ public function checkChatChannelSend(?User $user, Channel $channel): string
{
$prefix = 'chat.';

$this->ensureLoggedIn($user);
$this->ensureSessionVerified($user);
$this->ensureCleanRecord($user, $prefix);
// This check becomes useless when min_plays_allow_verified_bypass is enabled.
Expand Down Expand Up @@ -1836,14 +1837,41 @@ public function checkMultiplayerRoomCreate(?User $user): string

/**
* @param User|null $user
* @param Room $room
* @return string
* @throws AuthorizationCheckException
*/
public function checkMultiplayerScoreSubmit(?User $user): string
public function checkMultiplayerRoomDestroy(?User $user, Room $room): string
{
$prefix = 'room.destroy.';

$this->ensureLoggedIn($user);
$this->ensureCleanRecord($user);

if ($room->user_id !== $user->getKey()) {
return $prefix.'not_owner';
}

return 'ok';
}

/**
* @param User|null $user
* @return string
* @throws AuthorizationCheckException
*/
public function checkMultiplayerScoreSubmit(?User $user, Room $room): string
{
$this->ensureLoggedIn($user);

if ($room->isRealtime()) {
$this->ensureCleanRecord($user);
} else {
if ($user->isRestricted()) {
throw new AuthorizationCheckException('restricted');
}
}

return 'ok';
}

Expand Down Expand Up @@ -2023,8 +2051,6 @@ public function checkWikiPageRefresh(?User $user): string
}

/**
* @param User|null $user
* @param string $prefix
* @throws AuthorizationCheckException
*/
public function ensureLoggedIn(?User $user, string $prefix = ''): void
Expand All @@ -2035,35 +2061,25 @@ public function ensureLoggedIn(?User $user, string $prefix = ''): void
}

/**
* @param User|null $user
* @param string $prefix
* @return string
* @throws AuthorizationCheckException
*/
public function ensureCleanRecord(?User $user, string $prefix = ''): string
private function ensureCleanRecord(User $user, string $prefix = ''): void
{
if ($user === null) {
return 'unauthorized';
}

if ($user->isRestricted()) {
throw new AuthorizationCheckException($prefix.'restricted');
}

if ($user->isSilenced()) {
throw new AuthorizationCheckException($prefix.'silenced');
}

return 'ok';
}

/**
* @param User|null $user
* @throws AuthorizationCheckException
*/
public function ensureHasPlayed(?User $user): void
private function ensureHasPlayed(User $user): void
{
if ($user === null || $user->isBot()) {
if ($user->isBot()) {
return;
}

Expand All @@ -2087,13 +2103,10 @@ public function ensureHasPlayed(?User $user): void
/**
* Ensure User is logged in and verified.
*
* @param User|null $user
* @throws AuthorizationCheckException
*/
public function ensureSessionVerified(?User $user)
private function ensureSessionVerified(User $user)
{
$this->ensureLoggedIn($user);

if (!$user->isSessionVerified()) {
throw new AuthorizationCheckException('require_verification');
}
Expand Down
1 change: 1 addition & 0 deletions config/osu.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
],
'multiplayer' => [
'max_attempts_limit' => get_int(env('MULTIPLAYER_MAX_ATTEMPTS_LIMIT')) ?? 128,
'room_close_grace_period_minutes' => get_int(env('MULTIPLAYER_ROOM_CLOSE_GRACE_PERIOD_MINUTES')) ?? 5,
],
'notification' => [
'endpoint' => presence(env('NOTIFICATION_ENDPOINT'), '/home/notifications/feed'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// raw statement used as laravel syntax doesn't support a fixed size BINARY column
DB::statement('CREATE TABLE `beatmapset_files` (
`file_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`sha2_hash` BINARY(32) NOT NULL,
`file_size` INT UNSIGNED NOT NULL,
UNIQUE (`sha2_hash`)
)');

Schema::create('beatmapset_versions', function (Blueprint $table) {
$table->bigIncrements('version_id');
$table->mediumInteger('beatmapset_id')->unsigned();
$table->timestamp('created_at')->useCurrent();
$table->bigInteger('previous_version_id')->unsigned()->nullable();

$table->index('beatmapset_id');
$table->index('previous_version_id');
});

Schema::create('beatmapset_version_files', function (Blueprint $table) {
$table->bigInteger('file_id')->unsigned();
$table->bigInteger('version_id')->unsigned();
$table->string('filename', length: 500);

$table->primary(['file_id', 'version_id']);
$table->index('file_id');
$table->index('version_id');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('beatmapset_version_files');
Schema::dropIfExists('beatmapset_versions');
Schema::dropIfExists('beatmapset_files');
}
};
6 changes: 6 additions & 0 deletions resources/lang/en/authorization.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@
],
],

'room' => [
'destroy' => [
'not_owner' => 'Only room owner can close it.',
],
],

'score' => [
'pin' => [
'disabled_type' => "Can't pin this type of score",
Expand Down
2 changes: 1 addition & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@
});
});

Route::apiResource('rooms', 'Multiplayer\RoomsController', ['only' => ['index', 'show', 'store']]);
Route::apiResource('rooms', 'Multiplayer\RoomsController', ['only' => ['index', 'show', 'store', 'destroy']]);

Route::apiResource('seasonal-backgrounds', 'SeasonalBackgroundsController', ['only' => ['index']]);

Expand Down
88 changes: 88 additions & 0 deletions tests/Controllers/Multiplayer/RoomsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,94 @@ public function testJoinWithPassword()
$this->assertSame($initialUserChannelCount + 1, UserChannel::count());
}

public function testDestroy()
{
$start = now();
$end = $start->clone()->addMinutes(60);
$room = Room::factory()->create([
'starts_at' => $start,
'ends_at' => $end,
'type' => Room::PLAYLIST_TYPE,
]);
$end = $room->ends_at; // assignment truncates fractional second part, so refetch here
$url = route('api.rooms.destroy', ['room' => $room]);

$this->actAsScopedUser($room->host);
$this
->delete($url)
->assertSuccessful();

$room->refresh();
$this->assertLessThan($end, $room->ends_at);
}

public function testDestroyCannotBeCalledOnRealtimeRoom()
{
$start = now();
$end = $start->clone()->addMinutes(60);
$room = Room::factory()->create([
'starts_at' => $start,
'ends_at' => $end,
'type' => Room::REALTIME_DEFAULT_TYPE,
]);
$end = $room->ends_at; // assignment truncates fractional second part, so refetch here
$url = route('api.rooms.destroy', ['room' => $room]);

$this->actAsScopedUser($room->host);
$this
->delete($url)
->assertStatus(422);

$room->refresh();
$this->assertEquals($end, $room->ends_at);
}

public function testDestroyCannotBeCalledByAnotherUser()
{
$requester = User::factory()->create();
$owner = User::factory()->create();
$start = now();
$end = $start->clone()->addMinutes(60);
$room = Room::factory()->create([
'user_id' => $owner->getKey(),
'starts_at' => $start,
'ends_at' => $end,
'type' => Room::PLAYLIST_TYPE,
]);
$url = route('api.rooms.destroy', ['room' => $room]);
$end = $room->ends_at; // assignment truncates fractional second part, so refetch here

$this->actAsScopedUser($requester);
$this
->delete($url)
->assertStatus(403);

$room->refresh();
$this->assertEquals($end, $room->ends_at);
}

public function testDestroyCannotBeCalledAfterGracePeriod()
{
$start = now();
$end = $start->clone()->addMinutes(60);
$room = Room::factory()->create([
'starts_at' => $start,
'ends_at' => $end,
'type' => Room::PLAYLIST_TYPE,
]);
$url = route('api.rooms.destroy', ['room' => $room]);
$end = $room->ends_at; // assignment truncates fractional second part, so refetch here

$this->actAsScopedUser($room->host);
$this->travelTo($start->addMinutes(6));
$this
->delete($url)
->assertStatus(422);

$room->refresh();
$this->assertEquals($end, $room->ends_at);
}

public static function dataProviderForTestStoreWithInvalidPlayableMods(): array
{
$ret = [];
Expand Down
Loading

0 comments on commit 693fd4a

Please sign in to comment.