Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
36 changes: 36 additions & 0 deletions app/Http/Controllers/UpdateProjectLogoController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Http\Controllers;

use App\Models\Project;
use App\Services\ProjectService;
use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;

final class UpdateProjectLogoController extends AbstractController
{
/**
* @throws Exception
* @throws ValidationException
*/
public function __invoke(Request $request, int $project_id): RedirectResponse
{
$project = Project::find($project_id);
Gate::authorize('update', $project);

$request->validate([
'logo' => 'required|image',
]);

if ($project === null) {
throw new Exception();
}

ProjectService::setLogo($project, $request->file('logo'));

return response()->redirectTo(url("/projects/{$project->id}/settings"));
}
}
26 changes: 26 additions & 0 deletions app/Services/ProjectService.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
use App\Models\Project;
use App\Models\SubProject;
use App\Models\SubProjectGroup;
use CDash\Model\Image;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

Expand All @@ -28,6 +32,28 @@ public static function create(array $attributes): Project
return $project;
}

/**
* TODO: Rewrite this to use a dedicated project logo workflow instead of sharing the image table.
*
* @throws FileNotFoundException
*/
public static function setLogo(Project $project, UploadedFile $file): void
{
$contents = $file->get();
if ($contents === false) {
throw new Exception();
}

$image = new Image();
$image->Data = $contents;
$image->Checksum = crc32($contents);
$image->Extension = $file->extension();
$image->Save(true);

$project->imageid = (int) $image->Id;
$project->save();
}

/** This method is meant to be temporary, eventually only being called in create() */
public static function initializeBuildGroups(Project $project): void
{
Expand Down
2 changes: 2 additions & 0 deletions app/cdash/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ add_feature_test_in_transaction(/Feature/GraphQL/BuildTypeTest)

add_feature_test_in_transaction(/Feature/ProjectInvitationAcceptanceTest)

add_feature_test_in_transaction(/Feature/UpdateProjectLogoTest)

add_feature_test_in_transaction(/Feature/GraphQL/FilterTest)

add_feature_test_in_transaction(/Feature/GraphQL/QueryTypeTest)
Expand Down
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2472,6 +2472,12 @@ parameters:
count: 1
path: app/Http/Controllers/TimelineController.php

-
rawMessage: 'Parameter #2 $file of static method App\Services\ProjectService::setLogo() expects Illuminate\Http\UploadedFile, (array<int, Illuminate\Http\UploadedFile>|Illuminate\Http\UploadedFile|null) given.'
identifier: argument.type
count: 1
path: app/Http/Controllers/UpdateProjectLogoController.php

-
rawMessage: Access to an undefined property App\Models\Project::$pivot.
identifier: property.notFound
Expand Down
4 changes: 4 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use App\Http\Controllers\CreateProjectController;
use App\Http\Controllers\GlobalInvitationController;
use App\Http\Controllers\ProjectInvitationController;
use App\Http\Controllers\UpdateProjectLogoController;
use App\Models\DynamicAnalysis;
use App\Models\Project;
use App\Models\Test;
Expand Down Expand Up @@ -160,6 +161,9 @@
return redirect("/builds/{$buildid}/coverage/{$fileid}", 301);
});

Route::post('/projects/{project_id}/logo', UpdateProjectLogoController::class)
->whereNumber('project_id');

Route::get('/projects/{id}/edit', 'EditProjectController@edit')
->whereNumber('id');
Route::permanentRedirect('/project/{id}/edit', url('/projects/{id}/edit'));
Expand Down
129 changes: 129 additions & 0 deletions tests/Feature/UpdateProjectLogoTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Tests\Feature;

use App\Models\Project;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
use Tests\Traits\CreatesProjects;
use Tests\Traits\CreatesUsers;

class UpdateProjectLogoTest extends TestCase
{
use CreatesProjects;
use CreatesUsers;
use DatabaseTransactions;

public function testCannotUploadToNonExistentProject(): void
{
$user = $this->makeAdminUser();
$response = $this->actingAs($user)->postJson('/projects/123456789/logo', [
'logo' => UploadedFile::fake()->image('logo.jpg'),
]);
$response->assertForbidden();
}

public function testCannotUseNonIntegerProjectId(): void
{
$user = $this->makeAdminUser();
$response = $this->actingAs($user)->postJson('/projects/abc/logo', [
'logo' => UploadedFile::fake()->image('logo.jpg'),
]);
$response->assertNotFound();
}

public function testCannotUploadAsAnonymousUser(): void
{
$project = $this->makePublicProject();

$response = $this->postJson("/projects/{$project->id}/logo", [
'logo' => UploadedFile::fake()->image('logo.jpg'),
]);

$response->assertForbidden();
self::assertNull($project->fresh()?->logoUrl);
}

public function testCannotUploadAsNormalUser(): void
{
$project = $this->makePublicProject();
$user = $this->makeNormalUser();

$response = $this->actingAs($user)->postJson("/projects/{$project->id}/logo", [
'logo' => UploadedFile::fake()->image('logo.jpg'),
]);

$response->assertForbidden();
self::assertNull($project->fresh()?->logoUrl);
}

public function testCannotUploadAsProjectUser(): void
{
$project = $this->makePublicProject();
$user = $this->makeNormalUser();
$project->users()->attach($user, ['role' => Project::PROJECT_USER]);

$response = $this->actingAs($user)->postJson("/projects/{$project->id}/logo", [
'logo' => UploadedFile::fake()->image('logo.jpg'),
]);

$response->assertForbidden();
self::assertNull($project->fresh()?->logoUrl);
}

public function testCanUploadAsProjectAdmin(): void
{
Storage::fake('public');
$project = $this->makePublicProject();
$user = $this->makeNormalUser();
$project->users()->attach($user, ['role' => Project::PROJECT_ADMIN]);

$response = $this->actingAs($user)->postJson("/projects/{$project->id}/logo", [
'logo' => UploadedFile::fake()->image('logo.jpg'),
]);

$response->assertRedirect(url("/projects/{$project->id}/settings"));
self::assertNotNull($project->fresh()?->logoUrl);
}

public function testCanUploadAsGlobalAdmin(): void
{
$project = $this->makePublicProject();
$user = $this->makeAdminUser();

$response = $this->actingAs($user)->postJson("/projects/{$project->id}/logo", [
'logo' => UploadedFile::fake()->image('logo.jpg'),
]);

$response->assertRedirect(url("/projects/{$project->id}/settings"));
self::assertNotNull($project->fresh()?->logoUrl);
}

public function testCannotUploadNonImage(): void
{
$project = $this->makePublicProject();
$user = $this->makeAdminUser();

$response = $this->actingAs($user)->postJson("/projects/{$project->id}/logo", [
'logo' => UploadedFile::fake()->create('document.pdf'),
]);

$response->assertStatus(422);
self::assertNull($project->fresh()?->logoUrl);
}

public function testCannotUploadSvg(): void
{
$project = $this->makePublicProject();
$user = $this->makeAdminUser();

$response = $this->actingAs($user)->postJson("/projects/{$project->id}/logo", [
'logo' => UploadedFile::fake()->create('logo.svg'),
]);

$response->assertStatus(422);
self::assertNull($project->fresh()?->logoUrl);
}
}