From d9f2cb87d84a9b413b7f15b2456c81c7e1c213f3 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Mon, 9 Sep 2024 17:07:20 +0200 Subject: [PATCH 01/67] Development: Improve cleanup schedule for build logs --- .../artemis/service/BuildLogEntryService.java | 29 +++++++++++++++++-- .../resources/config/application-artemis.yml | 2 +- .../service/BuildLogEntryServiceTest.java | 4 --- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/BuildLogEntryService.java b/src/main/java/de/tum/in/www1/artemis/service/BuildLogEntryService.java index 9b2916219cd0..73198cbb7d52 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/BuildLogEntryService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/BuildLogEntryService.java @@ -40,15 +40,18 @@ public class BuildLogEntryService { private final ProgrammingSubmissionRepository programmingSubmissionRepository; + private final ProfileService profileService; + @Value("${artemis.continuous-integration.build-log.file-expiry-days:30}") private int expiryDays; @Value("${artemis.build-logs-path:./build-logs}") private Path buildLogsPath; - public BuildLogEntryService(BuildLogEntryRepository buildLogEntryRepository, ProgrammingSubmissionRepository programmingSubmissionRepository) { + public BuildLogEntryService(BuildLogEntryRepository buildLogEntryRepository, ProgrammingSubmissionRepository programmingSubmissionRepository, ProfileService profileService) { this.buildLogEntryRepository = buildLogEntryRepository; this.programmingSubmissionRepository = programmingSubmissionRepository; + this.profileService = profileService; } /** @@ -331,10 +334,30 @@ public FileSystemResource retrieveBuildLogsFromFileForBuildJob(String buildJobId } /** - * Deletes all build log files that are older than {@link #expiryDays} days on a schedule + * Scheduled task that deletes old build log files from the continuous integration system. + *

+ * This method runs based on the cron schedule defined in the application properties, with + * a default value of 3:00 AM every day if no custom schedule is provided. + * The task will only execute if scheduling is active, which is checked via the {@code profileService}. + *

+ *

+ * The method iterates through the files in the configured build logs directory and deletes + * files that were last modified before the configured expiry period (in days). The expiration + * period is specified by the {@code expiryDays} variable, and files older than this period are deleted. + *

+ *

+ * In case of an error during file deletion, it logs the error and continues processing. + *

+ * + * @throws IOException if an I/O error occurs while accessing the build log files directory or + * deleting files. */ - @Scheduled(cron = "${artemis.continuous-integration.build-log.cleanup-schedule:0 0 3 1 * ?}") + @Scheduled(cron = "${artemis.continuous-integration.build-log.cleanup-schedule:0 0 3 * * ?}") public void deleteOldBuildLogsFiles() { + // only execute this if scheduling is active + if (!profileService.isSchedulingActive()) { + return; + } log.info("Deleting old build log files"); ZonedDateTime now = ZonedDateTime.now(); diff --git a/src/main/resources/config/application-artemis.yml b/src/main/resources/config/application-artemis.yml index 9b2c2e3f40f7..f1501cbceba2 100644 --- a/src/main/resources/config/application-artemis.yml +++ b/src/main/resources/config/application-artemis.yml @@ -90,7 +90,7 @@ artemis: notification-plugin: "ls1tum/artemis-notification-plugin:1.0.0" # Docker image for the generic notification plugin. This value is set in an CI variable in GitLab CI. build-log: file-expiry-days: 30 # The amount of days until build log files can be deleted - cleanup-schedule: 0 0 3 1 * ? # Cron expression for schedule to delete old build log files + cleanup-schedule: 0 0 3 * * ? # Cron expression for schedule to delete old build log files git: name: Artemis email: artemis@xcit.tum.de diff --git a/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java index ba2befae81f0..398572dc7613 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java @@ -350,10 +350,6 @@ void filterOutEmptyLogs() { assertThat(result).isEmpty(); } - private List convertToBuildLogs(List content) { - return convertToBuildLogs(content.stream()); - } - private List convertToBuildLogs(String... content) { return convertToBuildLogs(Arrays.stream(content)); } From a902b4d3b8e415f358ca541fa95aa8ed6672b6f7 Mon Sep 17 00:00:00 2001 From: Lucas Welscher Date: Mon, 9 Sep 2024 23:04:59 +0200 Subject: [PATCH 02/67] Assessment: Don't allow activating presentations if no presentation scoring is set (#9251) --- .../file-upload/manage/file-upload-exercise.component.html | 4 ++-- .../modeling/manage/modeling-exercise.component.html | 4 ++-- .../programming/manage/programming-exercise.component.html | 4 ++-- .../shared/presentation-score/presentation-score.component.ts | 2 +- .../text/manage/text-exercise/text-exercise.component.html | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html index a478cc930b75..8e0258d925a9 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html @@ -16,7 +16,7 @@       - @if (course.presentationScore !== 0) { + @if (course.presentationScore) {   @@ -58,7 +58,7 @@ {{ fileUploadExercise.maxPoints }} {{ fileUploadExercise.bonusPoints }} {{ exerciseService.isIncludedInScore(fileUploadExercise) }} - @if (course.presentationScore !== 0) { + @if (course.presentationScore) { {{ fileUploadExercise.presentationScoreEnabled }} } {{ fileUploadExercise.filePattern }} diff --git a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html index e76a279bbb45..7658540cd82c 100644 --- a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html +++ b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html @@ -16,7 +16,7 @@       - @if (course.presentationScore !== 0) { + @if (course.presentationScore) {   @@ -58,7 +58,7 @@ {{ modelingExercise.maxPoints }} {{ modelingExercise.bonusPoints }} {{ exerciseService.isIncludedInScore(modelingExercise) }} - @if (course.presentationScore !== 0) { + @if (course.presentationScore) { {{ modelingExercise.presentationScoreEnabled }} } {{ modelingExercise.diagramType }} diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html index 39f4a28bbff0..098e76ae96b9 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html @@ -23,7 +23,7 @@ - @if (course.presentationScore !== 0) { + @if (course.presentationScore) {   @@ -151,7 +151,7 @@ - @if (course.presentationScore !== 0) { + @if (course.presentationScore) { {{ programmingExercise.presentationScoreEnabled }} } diff --git a/src/main/webapp/app/exercises/shared/presentation-score/presentation-score.component.ts b/src/main/webapp/app/exercises/shared/presentation-score/presentation-score.component.ts index 628e3eec033e..5b1ed0e98593 100644 --- a/src/main/webapp/app/exercises/shared/presentation-score/presentation-score.component.ts +++ b/src/main/webapp/app/exercises/shared/presentation-score/presentation-score.component.ts @@ -68,7 +68,7 @@ export class PresentationScoreComponent implements DoCheck, OnDestroy { } private isBasicPresentation(): boolean { - return !!(this.exercise.course && this.exercise.course.presentationScore !== 0); + return !!this.exercise.course?.presentationScore; } private isGradedPresentation(): boolean { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html index 379a4761e2e7..96efff611126 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html @@ -16,7 +16,7 @@       - @if (course.presentationScore !== 0) { + @if (course.presentationScore) {   @@ -53,7 +53,7 @@ {{ textExercise.maxPoints }} {{ textExercise.bonusPoints }} {{ exerciseService.isIncludedInScore(textExercise) }} - @if (course.presentationScore !== 0) { + @if (course.presentationScore) { {{ textExercise.presentationScoreEnabled }} } From a2d6daeee133b98d2667a652ef7b00a5af032040 Mon Sep 17 00:00:00 2001 From: Lucas Welscher Date: Mon, 9 Sep 2024 23:05:12 +0200 Subject: [PATCH 03/67] Exam mode: Show exercise group title in breadcrumbs (#9254) --- .../manage/course-management.service.ts | 2 +- ...gramming-exercise-participation.service.ts | 4 +-- .../shared/exercise/exercise.service.ts | 8 ++--- .../layouts/navbar/entity-title.service.ts | 11 +++++++ .../spec/service/entity-title.service.spec.ts | 31 +++++++++++++++++++ .../spec/service/exercise.service.spec.ts | 25 ++------------- 6 files changed, 50 insertions(+), 31 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 6b1a0a567412..0f442cada5ea 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -676,7 +676,7 @@ export class CourseManagementService { this.entityTitleService.setTitle(EntityType.COURSE, [course?.id], course?.title); course?.exercises?.forEach((exercise) => { - this.entityTitleService.setTitle(EntityType.EXERCISE, [exercise.id], exercise.title); + this.entityTitleService.setExerciseTitle(exercise); }); course?.lectures?.forEach((lecture) => this.entityTitleService.setTitle(EntityType.LECTURE, [lecture.id], lecture.title)); course?.exams?.forEach((exam) => this.entityTitleService.setTitle(EntityType.EXAM, [exam.id], exam.title)); diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts index 5953571a4190..e5ece788a0fb 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts @@ -3,11 +3,11 @@ import { Injectable } from '@angular/core'; import { AccountService } from 'app/core/auth/account.service'; import { Participation } from 'app/entities/participation/participation.model'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; +import { CommitInfo } from 'app/entities/programming/programming-submission.model'; import { Result } from 'app/entities/result.model'; import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service'; import { createRequestOption } from 'app/shared/util/request.util'; import { Observable, map, tap } from 'rxjs'; -import { CommitInfo } from 'app/entities/programming/programming-submission.model'; export interface IProgrammingExerciseParticipationService { getLatestResultWithFeedback: (participationId: number, withSubmission: boolean) => Observable; @@ -81,7 +81,7 @@ export class ProgrammingExerciseParticipationService implements IProgrammingExer sendTitlesToEntityTitleService(participation: Participation | undefined) { if (participation?.exercise) { const exercise = participation.exercise; - this.entityTitleService.setTitle(EntityType.EXERCISE, [exercise.id], exercise.title); + this.entityTitleService.setExerciseTitle(exercise); if (exercise.course) { const course = exercise.course; diff --git a/src/main/webapp/app/exercises/shared/exercise/exercise.service.ts b/src/main/webapp/app/exercises/shared/exercise/exercise.service.ts index a5c6b2febc1a..909aa30aa592 100644 --- a/src/main/webapp/app/exercises/shared/exercise/exercise.service.ts +++ b/src/main/webapp/app/exercises/shared/exercise/exercise.service.ts @@ -488,12 +488,8 @@ export class ExerciseService { } public sendExerciseTitleToTitleService(exercise?: Exercise) { - // we only want to show the exercise group name as exercise name to the student for exam exercises. - // for tutors and more privileged users, we want to show the exercise title - if (exercise?.exerciseGroup && !exercise?.isAtLeastTutor) { - this.entityTitleService.setTitle(EntityType.EXERCISE, [exercise?.id], exercise?.exerciseGroup.title); - } else { - this.entityTitleService.setTitle(EntityType.EXERCISE, [exercise?.id], exercise?.title); + if (exercise) { + this.entityTitleService.setExerciseTitle(exercise); } if (exercise?.course) { this.entityTitleService.setTitle(EntityType.COURSE, [exercise.course.id], exercise.course.title); diff --git a/src/main/webapp/app/shared/layouts/navbar/entity-title.service.ts b/src/main/webapp/app/shared/layouts/navbar/entity-title.service.ts index d342f8eb1464..a50aab27ddc4 100644 --- a/src/main/webapp/app/shared/layouts/navbar/entity-title.service.ts +++ b/src/main/webapp/app/shared/layouts/navbar/entity-title.service.ts @@ -1,6 +1,7 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { captureException } from '@sentry/angular'; +import { Exercise } from 'app/entities/exercise.model'; import { EMPTY, Observable, ReplaySubject, Subject } from 'rxjs'; export enum EntityType { @@ -88,6 +89,16 @@ export class EntityTitleService { } } + public setExerciseTitle(exercise: Exercise) { + // we only want to show the exercise group name as exercise name to the students for exam exercises. + // for tutors and more privileged users, we want to show the exercise title + if (exercise.exerciseGroup && !exercise?.isAtLeastTutor) { + this.setTitle(EntityType.EXERCISE, [exercise.id], exercise.exerciseGroup.title); + } else { + this.setTitle(EntityType.EXERCISE, [exercise.id], exercise.title); + } + } + /** * Fetches the title of the given entity from the server. * diff --git a/src/test/javascript/spec/service/entity-title.service.spec.ts b/src/test/javascript/spec/service/entity-title.service.spec.ts index 1e8165930ba7..a0d25c344ae6 100644 --- a/src/test/javascript/spec/service/entity-title.service.spec.ts +++ b/src/test/javascript/spec/service/entity-title.service.spec.ts @@ -1,3 +1,4 @@ +import { Exercise } from 'app/entities/exercise.model'; import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { MockHttpService } from '../helpers/mocks/service/mock-http.service'; @@ -115,4 +116,34 @@ describe('EntityTitleService', () => { service.setTitle(type, ids, title); expect(captureSpy).toHaveBeenCalledOnce(); }); + + it('sets the exercise group title for students during an exam', () => { + const exercise = { id: 1, exerciseGroup: { title: 'Group Title' }, isAtLeastTutor: false } as Exercise; + service.setExerciseTitle(exercise); + + let result: string | undefined = undefined; + service.getTitle(EntityType.EXERCISE, [1]).subscribe((title) => (result = title)); + + expect(result).toBe('Group Title'); + }); + + it('sets the exercise title for tutors and more privileged users', () => { + const exercise = { id: 1, exerciseGroup: { title: 'Group Title' }, isAtLeastTutor: true, title: 'Exercise Title' } as Exercise; + service.setExerciseTitle(exercise); + + let result: string | undefined = undefined; + service.getTitle(EntityType.EXERCISE, [1]).subscribe((title) => (result = title)); + + expect(result).toBe('Exercise Title'); + }); + + it('sets the exercise title for course exercises', () => { + const exercise = { id: 1, isAtLeastTutor: false, title: 'Exercise Title' } as Exercise; + service.setExerciseTitle(exercise); + + let result: string | undefined = undefined; + service.getTitle(EntityType.EXERCISE, [1]).subscribe((title) => (result = title)); + + expect(result).toBe('Exercise Title'); + }); }); diff --git a/src/test/javascript/spec/service/exercise.service.spec.ts b/src/test/javascript/spec/service/exercise.service.spec.ts index 415078a437e1..3213088306c1 100644 --- a/src/test/javascript/spec/service/exercise.service.spec.ts +++ b/src/test/javascript/spec/service/exercise.service.spec.ts @@ -22,7 +22,7 @@ import { SafeHtml } from '@angular/platform-browser'; import { ExerciseCategory } from 'app/entities/exercise-category.model'; import { Observable } from 'rxjs'; import { AccountService } from 'app/core/auth/account.service'; -import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service'; +import { EntityTitleService } from 'app/shared/layouts/navbar/entity-title.service'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; describe('Exercise Service', () => { @@ -358,7 +358,7 @@ describe('Exercise Service', () => { const profileService = TestBed.inject(ProfileService); const accountServiceSpy = jest.spyOn(accountService, 'setAccessRightsForExerciseAndReferencedCourse'); - const entityTitleServiceSpy = jest.spyOn(entityTitleService, 'setTitle'); + const entityTitleServiceSpy = jest.spyOn(entityTitleService, 'setExerciseTitle'); const profileServiceSpy = jest.spyOn(profileService, 'getProfileInfo'); const category = { @@ -387,7 +387,7 @@ describe('Exercise Service', () => { expect(accountServiceSpy).toHaveBeenCalledWith(expect.objectContaining({ id: exerciseFromServer.id })); expect(entityTitleServiceSpy).toHaveBeenCalledOnce(); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [exerciseFromServer.id], exerciseFromServer.title); + expect(entityTitleServiceSpy).toHaveBeenCalledWith(exerciseFromServer); expect(profileServiceSpy).not.toHaveBeenCalled(); }); @@ -520,23 +520,4 @@ describe('Exercise Service', () => { method: 'PUT', }); }); - - it('should correctly send the exercise name to the title service', () => { - const entityTitleService = TestBed.inject(EntityTitleService); - const examExerciseForStudent = { id: 1, title: 'exercise', exerciseGroup: { id: 1, title: 'exercise group' } } as Exercise; - const examExerciseForTutor = { ...examExerciseForStudent, isAtLeastTutor: true } as Exercise; - const courseExerciseForStudent = { ...examExerciseForStudent, exerciseGroup: undefined, course: { id: 2, title: 'course' } } as Exercise; - const courseExerciseForTutor = { ...courseExerciseForStudent, isAtLeastTutor: true } as Exercise; - const entityTitleServiceSpy = jest.spyOn(entityTitleService, 'setTitle'); - service.sendExerciseTitleToTitleService(examExerciseForStudent); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [1], 'exercise group'); - service.sendExerciseTitleToTitleService(examExerciseForTutor); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [1], 'exercise'); - service.sendExerciseTitleToTitleService(courseExerciseForStudent); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [1], 'exercise'); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.COURSE, [2], 'course'); - service.sendExerciseTitleToTitleService(courseExerciseForTutor); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [1], 'exercise'); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.COURSE, [2], 'course'); - }); }); From 9b517bcc57c48520d533ad788f610b91fcb64c22 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:05:31 +0200 Subject: [PATCH 04/67] Exam mode: Fix displaying incorrect task in exam summary exercise (#9294) --- .../programming-exercise-instruction.component.ts | 4 ++-- src/test/javascript/spec/helpers/sample/problemStatement.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts index f1f669c7fb5c..b0bd830496a9 100644 --- a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts @@ -348,14 +348,14 @@ export class ProgrammingExerciseInstructionComponent implements OnChanges, OnDes // Insert anchor divs into the text so that injectable elements can be inserted into them. // Without class="d-flex" the injected components height would be 0. // Added zero-width space as content so the div actually consumes a line to prevent a
    display bug in Safari - acc.replace(new RegExp(escapeStringForUseInRegex(task), 'g'), `
    `), + acc.replace(new RegExp(escapeStringForUseInRegex(task), 'g'), `
    `), problemStatementHtml, ); } private injectTasksIntoDocument = () => { this.tasks.forEach(({ id, taskName, testIds }) => { - const taskHtmlContainers = document.getElementsByClassName(`pe-task-${id}`); + const taskHtmlContainers = document.getElementsByClassName(`pe-${this.exercise.id}-task-${id}`); for (let i = 0; i < taskHtmlContainers.length; i++) { const taskHtmlContainer = taskHtmlContainers[i]; diff --git a/src/test/javascript/spec/helpers/sample/problemStatement.json b/src/test/javascript/spec/helpers/sample/problemStatement.json index 71494b6f9608..125a7c25d5c9 100644 --- a/src/test/javascript/spec/helpers/sample/problemStatement.json +++ b/src/test/javascript/spec/helpers/sample/problemStatement.json @@ -7,8 +7,8 @@ "problemStatementBothFailedRendered": "
      \n
    1. Implement Bubble Sort: artemisApp.editor.testStatusLabels.noResult
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge Sort: artemisApp.editor.testStatusLabels.noResult
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    \n", "problemStatementBothFailedHtml": "
      \n
    1. Implement Bubble Sort: artemisApp.editor.testStatusLabels.testFailing
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge Sort: artemisApp.editor.testStatusLabels.testPassing
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    \n", "problemStatementBubbleSortFailsRendered": "
      \n
    1. Implement Bubble Sort: artemisApp.editor.testStatusLabels.noResult
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge Sort: artemisApp.editor.testStatusLabels.noResult
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    \n", - "problemStatementBubbleSortNotExecutedHtml": "
      \n
    1. Implement Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":0}]
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    ", + "problemStatementBubbleSortNotExecutedHtml": "
      \n
    1. Implement Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":0}]
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    ", "problemStatementEmptySecondTask": "1. [task][Bubble Sort](1) \n Implement the method. \n 2. [task][Merge Sort]() \n Implement the method.", - "problemStatementEmptySecondTaskNotExecutedHtml": "
      \n
    1. Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
      \nImplement the method.
    2. \n
    3. Merge SortartemisApp.editor.testStatusLabels.noTests
      \nImplement the method.
    4. \n
    ", + "problemStatementEmptySecondTaskNotExecutedHtml": "
      \n
    1. Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
      \nImplement the method.
    2. \n
    3. Merge SortartemisApp.editor.testStatusLabels.noTests
      \nImplement the method.
    4. \n
    ", "problemStatementPlantUMLWithTest": "@startuml\nclass Policy {\n1)>+configure()\n2)>+testWithParenthesis()}\n@enduml" } From 8b80d61ed3e52837b1d2a051fdcf3dee8e3b30ae Mon Sep 17 00:00:00 2001 From: Yannik Schmidt Date: Mon, 9 Sep 2024 23:06:10 +0200 Subject: [PATCH 05/67] Programming exercises: Fix translation issue for participation modes in exam management (#9293) --- ...programming-exercise-group-cell.component.html | 15 ++++++++++----- .../programming-exercise-group-cell.component.ts | 4 +++- .../manage/programming-exercise.component.html | 14 ++++++++------ .../manage/programming-exercise.component.ts | 4 +++- ...gramming-exercise-group-cell.component.spec.ts | 12 +++++++----- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html index e59af00e81b9..5de7c8465f1a 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html +++ b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html @@ -84,14 +84,19 @@ @if (displayEditorModus) {
    - : {{ programmingExercise.allowOfflineIde || false }} + +
    - : {{ programmingExercise.allowOnlineEditor || false }} -
    -
    - : {{ programmingExercise.allowOnlineIde || false }} + +
    + @if (onlineIdeEnabled) { +
    + + +
    + }
    } } diff --git a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts index a2b6d64ca6e0..a171fc11d30e 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts +++ b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts @@ -9,7 +9,7 @@ import { ProgrammingExerciseInstructorRepositoryType, ProgrammingExerciseService import { downloadZipFileFromResponse } from 'app/shared/util/download.util'; import { AlertService } from 'app/core/util/alert.service'; import { faDownload } from '@fortawesome/free-solid-svg-icons'; -import { PROFILE_LOCALVC } from 'app/app.constants'; +import { PROFILE_LOCALVC, PROFILE_THEIA } from 'app/app.constants'; @Component({ selector: 'jhi-programming-exercise-group-cell', @@ -22,6 +22,7 @@ export class ProgrammingExerciseGroupCellComponent implements OnInit { programmingExercise: ProgrammingExercise; localVCEnabled = false; + onlineIdeEnabled = false; @Input() displayShortName = false; @@ -48,6 +49,7 @@ export class ProgrammingExerciseGroupCellComponent implements OnInit { ngOnInit(): void { this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + this.onlineIdeEnabled = profileInfo.activeProfiles.includes(PROFILE_THEIA); if (this.programmingExercise.projectKey) { if (this.programmingExercise.solutionParticipation?.buildPlanId) { this.programmingExercise.solutionParticipation!.buildPlanUrl = createBuildPlanUrl( diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html index 098e76ae96b9..de82f5d4dac1 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html @@ -139,17 +139,19 @@ }
    - : +
    - : +
    -
    - : - -
    + @if (onlineIdeEnabled) { +
    + + +
    + } @if (course.presentationScore) { {{ programmingExercise.presentationScoreEnabled }} diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts index e40533f526cc..bcc83f36839e 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts @@ -39,7 +39,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; import { downloadZipFileFromResponse } from 'app/shared/util/download.util'; -import { PROFILE_LOCALCI, PROFILE_LOCALVC } from 'app/app.constants'; +import { PROFILE_LOCALCI, PROFILE_LOCALVC, PROFILE_THEIA } from 'app/app.constants'; @Component({ selector: 'jhi-programming-exercise', @@ -55,6 +55,7 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O // Used to make the repository links download the repositories instead of linking to GitLab. localVCEnabled = false; localCIEnabled = false; + onlineIdeEnabled = false; // extension points, see shared/extension-point @ContentChild('overrideRepositoryAndBuildPlan') overrideRepositoryAndBuildPlan: TemplateRef; @@ -111,6 +112,7 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O this.buildPlanLinkTemplate = profileInfo.buildPlanURLTemplate; this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); this.localCIEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALCI); + this.onlineIdeEnabled = profileInfo.activeProfiles.includes(PROFILE_THEIA); }); // reconnect exercise with course this.programmingExercises.forEach((exercise) => { diff --git a/src/test/javascript/spec/component/exam/manage/exercise-groups/programming-exercise-group-cell.component.spec.ts b/src/test/javascript/spec/component/exam/manage/exercise-groups/programming-exercise-group-cell.component.spec.ts index 499338bffb52..4fa017b56649 100644 --- a/src/test/javascript/spec/component/exam/manage/exercise-groups/programming-exercise-group-cell.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/exercise-groups/programming-exercise-group-cell.component.spec.ts @@ -11,6 +11,8 @@ import { of } from 'rxjs'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { AlertService } from 'app/core/util/alert.service'; import { MockAlertService } from '../../../../helpers/mocks/service/mock-alert.service'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { PROFILE_THEIA } from 'app/app.constants'; describe('Programming Exercise Group Cell Component', () => { let comp: ProgrammingExerciseGroupCellComponent; @@ -43,12 +45,12 @@ describe('Programming Exercise Group Cell Component', () => { // @ts-ignore of({ buildPlanURLTemplate: 'https://example.com/{buildPlanId}/{projectKey}', - activeProfiles: [], + activeProfiles: [PROFILE_THEIA], }), }; TestBed.configureTestingModule({ - imports: [ArtemisTestModule], + imports: [ArtemisTestModule, ArtemisSharedCommonModule], declarations: [ProgrammingExerciseGroupCellComponent, TranslatePipeMock], providers: [ { provide: ProfileService, useValue: mockedProfileService }, @@ -94,15 +96,15 @@ describe('Programming Exercise Group Cell Component', () => { const div0 = fixture.debugElement.query(By.css('div > div > div:first-child')); expect(div0).not.toBeNull(); - expect(div0.nativeElement.textContent?.trim()).toBe(': true'); + expect(div0.nativeElement.textContent?.trim()).toBe('artemisApp.programmingExercise.offlineIdeartemisApp.exercise.yes'); const div1 = fixture.debugElement.query(By.css('div > div > div:nth-child(2)')); expect(div1).not.toBeNull(); - expect(div1.nativeElement.textContent?.trim()).toBe(': true'); + expect(div1.nativeElement.textContent?.trim()).toBe('artemisApp.programmingExercise.onlineEditorartemisApp.exercise.yes'); const div2 = fixture.debugElement.query(By.css('div > div > div:nth-child(3)')); expect(div2).not.toBeNull(); - expect(div2.nativeElement.textContent?.trim()).toBe(': false'); + expect(div2.nativeElement.textContent?.trim()).toBe('artemisApp.programmingExercise.onlineIdeartemisApp.exercise.no'); }); it('should download the repository', () => { From 921b1884fb1a3028512e61023f254f1365fec14b Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Tue, 10 Sep 2024 09:16:13 +0200 Subject: [PATCH 06/67] Development: Update client dependencies --- jest.config.js | 8 +- package-lock.json | 448 +++++++++++++++++++++++++++------------------- package.json | 20 +-- 3 files changed, 275 insertions(+), 201 deletions(-) diff --git a/jest.config.js b/jest.config.js index 45ff00b5e3ce..882d9a3c7d35 100644 --- a/jest.config.js +++ b/jest.config.js @@ -102,10 +102,10 @@ module.exports = { coverageThreshold: { global: { // TODO: in the future, the following values should increase to at least 90% - statements: 87.22, - branches: 73.40, - functions: 81.54, - lines: 87.29, + statements: 87.39, + branches: 73.61, + functions: 81.93, + lines: 87.45, }, }, coverageReporters: ['clover', 'json', 'lcov', 'text-summary'], diff --git a/package-lock.json b/package-lock.json index f05b86d4cc8f..0131e10e4aa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/service-worker": "18.2.3", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", - "@fingerprintjs/fingerprintjs": "4.4.3", + "@fingerprintjs/fingerprintjs": "4.5.0", "@flaviosantoro92/ngx-datatable": "1.0.2", "@fortawesome/angular-fontawesome": "0.15.0", "@fortawesome/fontawesome-svg-core": "6.6.0", @@ -34,7 +34,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular": "8.28.0", + "@sentry/angular": "8.29.0", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", @@ -60,7 +60,7 @@ "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", "pdfjs-dist": "4.6.82", - "posthog-js": "1.160.3", + "posthog-js": "1.161.2", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -87,7 +87,7 @@ "@angular/cli": "18.2.3", "@angular/compiler-cli": "18.2.3", "@angular/language-service": "18.2.3", - "@sentry/types": "8.28.0", + "@sentry/types": "8.29.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -99,9 +99,9 @@ "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.4.0", - "@typescript-eslint/parser": "8.4.0", - "eslint": "9.9.1", + "@typescript-eslint/eslint-plugin": "8.5.0", + "@typescript-eslint/parser": "8.5.0", + "eslint": "9.10.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.8.3", @@ -3422,9 +3422,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", - "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", + "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", "dev": true, "license": "MIT", "engines": { @@ -3441,10 +3441,23 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", + "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@fingerprintjs/fingerprintjs": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-4.4.3.tgz", - "integrity": "sha512-sm0ZmDp5Oeq8hQTf+bAHKsuuteVAYme/YOY9UPP/GrUBrR5Fzl1P5oOv6F5LvyBrO7qLjU5HQkfU0MmFte/8xA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-4.5.0.tgz", + "integrity": "sha512-mFSQoxyt8SGGRp1QUlhcnVtquW2HzCKfHKxAoIurR6soIJpuK3VvZuH0sg8eNaHH2dJhI3mZOEUx4k+P4GqXzw==", "license": "BUSL-1.1", "dependencies": { "tslib": "^2.4.1" @@ -3792,7 +3805,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3810,7 +3823,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -3823,7 +3836,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -3836,14 +3849,14 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -3861,7 +3874,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -3877,7 +3890,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -5865,73 +5878,73 @@ } }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.28.0.tgz", - "integrity": "sha512-tE9++KEy8SlqibTmYymuxFVAnutsXBqrwQ936WJbjaMfkqXiro7C1El0ybkprskd0rKS7kln20Q6nQlNlMEoTA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.29.0.tgz", + "integrity": "sha512-6HpyQkaqPvK6Lnigjlarq/LDYgXT2OBNf24RK7z0ipJSxSIpmtelfzHbnwWYnypNDXfTDdPm97fZEenQHryYJA==", "license": "MIT", "dependencies": { - "@sentry/core": "8.28.0", - "@sentry/types": "8.28.0", - "@sentry/utils": "8.28.0" + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.28.0.tgz", - "integrity": "sha512-5vYunPCDBLCJ8QNnhepacdYheiN+UtYxpGAIaC/zjBC1nDuBgWs+TfKPo1UlO/1sesfgs9ibpxtShOweucL61g==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.29.0.tgz", + "integrity": "sha512-yAL5YMEFk4XaeVRUGEguydahRzaQrNPAaWRv6k+XRzCv9CGBhxb14KXQc9X/penlauMFcDfgelCPKcTqcf6wDw==", "license": "MIT", "dependencies": { - "@sentry/core": "8.28.0", - "@sentry/types": "8.28.0", - "@sentry/utils": "8.28.0" + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.28.0.tgz", - "integrity": "sha512-70jvzzOL5O74gahgXKyRkZgiYN93yly5gq+bbj4/6NRQ+EtPd285+ccy0laExdfyK0ugvvwD4v+1MQit52OAsg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.29.0.tgz", + "integrity": "sha512-Xgv/eYucsm7GaGKms2ClQ02NpD07MxjoTjp1/vYZm0H4Q08dVphVZrQp7hL1oX/VD9mb5SFyyKuuIRqIu7S8RA==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.28.0", - "@sentry/core": "8.28.0", - "@sentry/types": "8.28.0", - "@sentry/utils": "8.28.0" + "@sentry-internal/browser-utils": "8.29.0", + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.28.0.tgz", - "integrity": "sha512-RfpYHDHMUKGeEdx41QtHITjEn6P3tGaDPHvatqdrD3yv4j+wbJ6laX1PrIxCpGFUtjdzkqi/KUcvUd2kzbH/FA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.29.0.tgz", + "integrity": "sha512-W2YbZRvp2lYC50V51fNLcnoIiK1Km4vSc+v6SL7c//lv2qpyumoUAAIDKY+14s8Lgt1RsR6rfZhfheD4O/6WSQ==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "8.28.0", - "@sentry/core": "8.28.0", - "@sentry/types": "8.28.0", - "@sentry/utils": "8.28.0" + "@sentry-internal/replay": "8.29.0", + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/angular": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.28.0.tgz", - "integrity": "sha512-zHl0OSgBsHnQCINepRxYDsosvKnwJPc9tdRJyIgQ6JCG1kWZf0lHncXRnJBkBSrJk2wJQ0acondhwHRyAptRGg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.29.0.tgz", + "integrity": "sha512-uxzWPopyMfHaVlxUwU5DEaAjryaA7DRE6h91lR5jHCIqI+puk4BPfEw1TWteoh/3BaP5PlFh/kLWQMJbA85jlg==", "license": "MIT", "dependencies": { - "@sentry/browser": "8.28.0", - "@sentry/core": "8.28.0", - "@sentry/types": "8.28.0", - "@sentry/utils": "8.28.0", + "@sentry/browser": "8.29.0", + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0", "tslib": "^2.4.1" }, "engines": { @@ -5945,52 +5958,52 @@ } }, "node_modules/@sentry/browser": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.28.0.tgz", - "integrity": "sha512-i/gjMYzIGQiPFH1pCbdnTwH9xs9mTAqzN+goP3GWX5a58frc7h8vxyA/5z0yMd0aCW6U8mVxnoAT72vGbKbx0g==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.29.0.tgz", + "integrity": "sha512-aKTy4H/3RI0q9LIeepesjWGlGNeh4HGFfwQjzHME8gcWCQ5LSlzYX4U+hu2yp7r1Jfd9MUTFfOuuLih2HGLGsQ==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.28.0", - "@sentry-internal/feedback": "8.28.0", - "@sentry-internal/replay": "8.28.0", - "@sentry-internal/replay-canvas": "8.28.0", - "@sentry/core": "8.28.0", - "@sentry/types": "8.28.0", - "@sentry/utils": "8.28.0" + "@sentry-internal/browser-utils": "8.29.0", + "@sentry-internal/feedback": "8.29.0", + "@sentry-internal/replay": "8.29.0", + "@sentry-internal/replay-canvas": "8.29.0", + "@sentry/core": "8.29.0", + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/core": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.28.0.tgz", - "integrity": "sha512-+If9uubvpZpvaQQw4HLiKPhrSS9/KcoA/AcdQkNm+5CVwAoOmDPtyYfkPBgfo2hLZnZQqR1bwkz/PrNoOm+gqA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.29.0.tgz", + "integrity": "sha512-scMbZaJ0Ov8NPgWn86EdjhyTLrhvRVbTxjg0imJAvhIvRbblH3xyqye/17Qnk2fOp8TNDOl7TBZHi0NCFQ5HUw==", "license": "MIT", "dependencies": { - "@sentry/types": "8.28.0", - "@sentry/utils": "8.28.0" + "@sentry/types": "8.29.0", + "@sentry/utils": "8.29.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/types": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.28.0.tgz", - "integrity": "sha512-hOfqfd92/AzBrEdMgmmV1VfOXJbIfleFTnerRl0mg/+CcNgP/6+Fdonp354TD56ouWNF2WkOM6sEKSXMWp6SEQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.29.0.tgz", + "integrity": "sha512-j4gX3ctzgD4xVWllXAhm6M+kHFEvrFoUPFq60X/pgkjsWCocGuhtNfB0rW43ICG8hCnlz8IYl7O7b8V8qY7SPg==", "license": "MIT", "engines": { "node": ">=14.18" } }, "node_modules/@sentry/utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.28.0.tgz", - "integrity": "sha512-smhk7PJpvDMQ2DB5p2qn9UeoUHdU41IgjMmS2xklZpa8tjzBTxDeWpGvrX2fuH67D9bAJuLC/XyZjJCHLoEW5g==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-nb93/m3SjQChQJFqJj3oNW3Rz/12yrT7jypTCire3c2hpYWG2uR5n8VY9UUMTA6HLNvdom6tckK7p3bXGXlF0w==", "license": "MIT", "dependencies": { - "@sentry/types": "8.28.0" + "@sentry/types": "8.29.0" }, "engines": { "node": ">=14.18" @@ -6777,17 +6790,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.4.0.tgz", - "integrity": "sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", + "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.4.0", - "@typescript-eslint/type-utils": "8.4.0", - "@typescript-eslint/utils": "8.4.0", - "@typescript-eslint/visitor-keys": "8.4.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/type-utils": "8.5.0", + "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6811,16 +6824,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.4.0.tgz", - "integrity": "sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", + "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.4.0", - "@typescript-eslint/types": "8.4.0", - "@typescript-eslint/typescript-estree": "8.4.0", - "@typescript-eslint/visitor-keys": "8.4.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", "debug": "^4.3.4" }, "engines": { @@ -6840,14 +6853,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz", - "integrity": "sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", + "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.4.0", - "@typescript-eslint/visitor-keys": "8.4.0" + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6858,14 +6871,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.4.0.tgz", - "integrity": "sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", + "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.4.0", - "@typescript-eslint/utils": "8.4.0", + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/utils": "8.5.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -6883,9 +6896,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.4.0.tgz", - "integrity": "sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", + "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", "dev": true, "license": "MIT", "engines": { @@ -6897,14 +6910,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz", - "integrity": "sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", + "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.4.0", - "@typescript-eslint/visitor-keys": "8.4.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6926,16 +6939,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.4.0.tgz", - "integrity": "sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.4.0", - "@typescript-eslint/types": "8.4.0", - "@typescript-eslint/typescript-estree": "8.4.0" + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6949,13 +6962,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz", - "integrity": "sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", + "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/types": "8.5.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7941,9 +7954,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "license": "MIT", "dependencies": { @@ -7955,7 +7968,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -7975,6 +7988,22 @@ "node": ">= 0.8" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -8016,7 +8045,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -8698,7 +8727,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/connect-history-api-fallback": { @@ -9095,7 +9124,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -9911,7 +9940,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/ee-first": { @@ -10198,9 +10227,9 @@ } }, "node_modules/eslint": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", - "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", + "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", "dev": true, "license": "MIT", "dependencies": { @@ -10208,7 +10237,8 @@ "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.18.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.9.1", + "@eslint/js": "9.10.0", + "@eslint/plugin-kit": "^0.1.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -10231,7 +10261,6 @@ "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", @@ -11046,38 +11075,38 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -11088,6 +11117,16 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -11405,7 +11444,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -11497,7 +11536,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -11681,7 +11720,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -11721,7 +11760,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -11732,7 +11771,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -12384,7 +12423,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -12710,7 +12749,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/ismobilejs-es5": { @@ -15938,11 +15977,14 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -16125,7 +16167,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -17297,7 +17339,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0" }, "node_modules/pacote": { @@ -17458,7 +17500,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17468,7 +17510,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -17506,9 +17548,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true, "license": "MIT" }, @@ -17873,9 +17915,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.160.3", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.160.3.tgz", - "integrity": "sha512-mGvxOIlWPtdPx8EI0MQ81wNKlnH2K0n4RqwQOl044b34BCKiFVzZ7Hc7geMuZNaRAvCi5/5zyGeWHcAYZQxiMQ==", + "version": "1.161.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.161.2.tgz", + "integrity": "sha512-2tgGFQm5c1BWY2nXP5xtvWl90EUyVsvaX0pxPYR6a0nP7XkgnhlIGPwxizeP41n/xQYmN3ZCLf8trjxJ8JRPKQ==", "license": "MIT", "dependencies": { "fflate": "^0.4.8", @@ -18657,7 +18699,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "glob": "^11.0.0", @@ -18677,7 +18719,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -18701,7 +18743,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -18720,7 +18762,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": "20 || >=22" @@ -18730,7 +18772,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -18746,7 +18788,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -19021,9 +19063,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "license": "MIT", "dependencies": { @@ -19132,9 +19174,9 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "dev": true, "license": "MIT", "dependencies": { @@ -19147,6 +19189,38 @@ "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -19219,7 +19293,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -19232,7 +19306,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -19322,7 +19396,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=14" @@ -19802,7 +19876,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -19817,14 +19891,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -19876,7 +19950,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -21971,7 +22045,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -22062,7 +22136,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -22080,7 +22154,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -22096,7 +22170,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -22109,21 +22183,21 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -22133,7 +22207,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", diff --git a/package.json b/package.json index ab806fb582e0..94177e7ee86e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@angular/service-worker": "18.2.3", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", - "@fingerprintjs/fingerprintjs": "4.4.3", + "@fingerprintjs/fingerprintjs": "4.5.0", "@flaviosantoro92/ngx-datatable": "1.0.2", "@fortawesome/angular-fontawesome": "0.15.0", "@fortawesome/fontawesome-svg-core": "6.6.0", @@ -37,7 +37,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular": "8.28.0", + "@sentry/angular": "8.29.0", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", @@ -63,7 +63,7 @@ "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", "pdfjs-dist": "4.6.82", - "posthog-js": "1.160.3", + "posthog-js": "1.161.2", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -88,16 +88,16 @@ "d3-transition": "^3.0.1" }, "@typescript-eslint/utils": { - "eslint": "^9.9.0" + "eslint": "^9.10.0" }, "braces": "3.0.3", "critters": "0.0.24", "debug": "4.3.6", "eslint-plugin-deprecation": { - "eslint": "^9.9.0" + "eslint": "^9.10.0" }, "eslint-plugin-jest": { - "@typescript-eslint/eslint-plugin": "^8.4.0" + "@typescript-eslint/eslint-plugin": "^8.5.0" }, "jsdom": "24.1.1", "katex": "0.16.11", @@ -124,7 +124,7 @@ "@angular/cli": "18.2.3", "@angular/compiler-cli": "18.2.3", "@angular/language-service": "18.2.3", - "@sentry/types": "8.28.0", + "@sentry/types": "8.29.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -136,9 +136,9 @@ "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.4.0", - "@typescript-eslint/parser": "8.4.0", - "eslint": "9.9.1", + "@typescript-eslint/eslint-plugin": "8.5.0", + "@typescript-eslint/parser": "8.5.0", + "eslint": "9.10.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", "eslint-plugin-jest": "28.8.3", From 46c2fd73de074b2b9a75b46407bc8f29e15b6c42 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Tue, 10 Sep 2024 09:33:26 +0200 Subject: [PATCH 07/67] Development: Try to fix flaky testPriorityRunningExam --- .../tum/in/www1/artemis/domain/BuildJob.java | 8 ++++ .../artemis/domain/exam/ExerciseGroup.java | 4 ++ .../repository/BuildJobRepository.java | 23 +---------- .../localci/SharedQueueManagementService.java | 2 +- ...ctSpringIntegrationLocalCILocalVCTest.java | 9 +++++ ...AbstractLocalCILocalVCIntegrationTest.java | 10 +++-- .../BuildAgentDockerServiceTest.java | 10 ----- .../localvcci/LocalCIIntegrationTest.java | 4 -- .../LocalCIResourceIntegrationTest.java | 6 +-- .../artemis/localvcci/LocalCIServiceTest.java | 2 + .../LocalVCLocalCIIntegrationTest.java | 38 +++++++++++-------- ... => SharedQueueManagementServiceTest.java} | 10 ----- 12 files changed, 56 insertions(+), 70 deletions(-) rename src/test/java/de/tum/in/www1/artemis/localvcci/{SharedQueueManagementService.java => SharedQueueManagementServiceTest.java} (89%) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/BuildJob.java b/src/main/java/de/tum/in/www1/artemis/domain/BuildJob.java index 08e805e540f4..fad90ce6c300 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/BuildJob.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/BuildJob.java @@ -235,4 +235,12 @@ public String getDockerImage() { public void setDockerImage(String dockerImage) { this.dockerImage = dockerImage; } + + @Override + public String toString() { + return "BuildJob{" + "buildJobId='" + buildJobId + "'" + ", name='" + name + "'" + ", exerciseId=" + exerciseId + ", courseId=" + courseId + ", participationId=" + + participationId + ", buildAgentAddress='" + buildAgentAddress + "'" + ", buildStartDate=" + buildStartDate + ", buildCompletionDate=" + buildCompletionDate + + ", repositoryType=" + repositoryType + ", repositoryName='" + repositoryName + "'" + ", commitHash='" + commitHash + "'" + ", retryCount=" + retryCount + + ", priority=" + priority + ", triggeredByPushTo=" + triggeredByPushTo + ", buildStatus=" + buildStatus + ", dockerImage='" + dockerImage + "'" + "}"; + } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/exam/ExerciseGroup.java b/src/main/java/de/tum/in/www1/artemis/domain/exam/ExerciseGroup.java index 05afe46bdbdb..5cec00825a8b 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/exam/ExerciseGroup.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/exam/ExerciseGroup.java @@ -80,4 +80,8 @@ public void addExercise(Exercise exercise) { this.exercises.add(exercise); } + @Override + public String toString() { + return "ExerciseGroup{" + "title='" + title + "'" + ", isMandatory=" + isMandatory + "}"; + } } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java index 23fe8dbaa66e..5f65feded254 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java @@ -35,30 +35,9 @@ public interface BuildJobRepository extends ArtemisJpaRepository Optional findBuildJobByResult(Result result); - @Query(""" - SELECT b.id - FROM BuildJob b - """) - List findAllIds(Pageable pageable); - @EntityGraph(type = LOAD, attributePaths = { "result", "result.participation", "result.participation.exercise", "result.submission" }) List findWithDataByIdIn(List ids); - /** - * Retrieves a paginated list of all {@link BuildJob} entities. - * - * @param pageable the pagination information. - * @return a paginated list of {@link BuildJob} entities. If no entities are found, returns an empty page. - */ - default Page findAllWithData(Pageable pageable) { - List ids = findAllIds(pageable); - if (ids.isEmpty()) { - return Page.empty(pageable); - } - List result = findWithDataByIdIn(ids); - return new PageImpl<>(result, pageable, count()); - } - // Cast to string is necessary. Otherwise, the query will fail on PostgreSQL. @Query(""" SELECT b.id @@ -74,7 +53,7 @@ default Page findAllWithData(Pageable pageable) { AND (:durationUpper IS NULL OR (b.buildCompletionDate - b.buildStartDate) <= :durationUpper) """) - Page findAllByFilterCriteria(@Param("buildStatus") BuildStatus buildStatus, @Param("buildAgentAddress") String buildAgentAddress, + Page findIdsByFilterCriteria(@Param("buildStatus") BuildStatus buildStatus, @Param("buildAgentAddress") String buildAgentAddress, @Param("startDate") ZonedDateTime startDate, @Param("endDate") ZonedDateTime endDate, @Param("searchTerm") String searchTerm, @Param("courseId") Long courseId, @Param("durationLower") Duration durationLower, @Param("durationUpper") Duration durationUpper, Pageable pageable); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java index 939fa41cee19..5aeffb5e4316 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java @@ -261,7 +261,7 @@ public Page getFilteredFinishedBuildJobs(FinishedBuildJobPageableSearc sortOptions = search.pageable().getSortingOrder() == SortingOrder.ASCENDING ? sortOptions.ascending() : sortOptions.descending(); var pageRequest = PageRequest.of(search.pageable().getPage() - 1, search.pageable().getPageSize(), sortOptions); - Page buildJobIdsPage = buildJobRepository.findAllByFilterCriteria(search.buildStatus(), search.buildAgentAddress(), search.startDate(), search.endDate(), + Page buildJobIdsPage = buildJobRepository.findIdsByFilterCriteria(search.buildStatus(), search.buildAgentAddress(), search.startDate(), search.endDate(), search.pageable().getSearchTerm(), courseId, buildDurationLower, buildDurationUpper, pageRequest); List buildJobIds = buildJobIdsPage.toList(); diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java index 056a664010ef..d1dcf21dc778 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java @@ -36,6 +36,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.localvcci.LocalVCLocalCITestService; import de.tum.in.www1.artemis.localvcci.TestBuildAgentConfiguration; +import de.tum.in.www1.artemis.repository.BuildJobRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; @@ -102,6 +103,9 @@ public abstract class AbstractSpringIntegrationLocalCILocalVCTest extends Abstra @Autowired protected UserUtilService userUtilService; + @Autowired + protected BuildJobRepository buildJobRepository; + /** * This is the mock(DockerClient.class) provided by the {@link TestBuildAgentConfiguration}. * Subclasses can use this to dynamically mock methods of the DockerClient. @@ -162,6 +166,11 @@ protected void resetSpyBeans() { super.resetSpyBeans(); } + @AfterEach + void clearBuildJobs() { + buildJobRepository.deleteAll(); + } + /** * Note: Mocking requests to the VC and CI server is not necessary for local VC and local CI. * The VC system is part of the application context and can thus be called directly. diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/AbstractLocalCILocalVCIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/AbstractLocalCILocalVCIntegrationTest.java index 4d044a370a85..35cbbb30ae10 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/AbstractLocalCILocalVCIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/AbstractLocalCILocalVCIntegrationTest.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Set; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -23,7 +24,6 @@ import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; import de.tum.in.www1.artemis.repository.ExamRepository; -import de.tum.in.www1.artemis.repository.ExerciseGroupRepository; import de.tum.in.www1.artemis.repository.StudentExamRepository; import de.tum.in.www1.artemis.repository.TeamRepository; import de.tum.in.www1.artemis.service.StaticCodeAnalysisService; @@ -37,9 +37,6 @@ public class AbstractLocalCILocalVCIntegrationTest extends AbstractSpringIntegra @Autowired protected TeamRepository teamRepository; - @Autowired - protected ExerciseGroupRepository exerciseGroupRepository; - @Autowired protected ExamRepository examRepository; @@ -163,4 +160,9 @@ void initUsersAndExercise() throws JsonProcessingException { localVCLocalCITestService.addTestCases(programmingExercise); } + + @AfterEach + void tearDown() { + buildJobRepository.deleteAll(); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/BuildAgentDockerServiceTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/BuildAgentDockerServiceTest.java index bb953048f55e..35116c968bc3 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/BuildAgentDockerServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/BuildAgentDockerServiceTest.java @@ -11,7 +11,6 @@ import java.time.ZonedDateTime; import java.util.List; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -33,7 +32,6 @@ import de.tum.in.www1.artemis.domain.BuildJob; import de.tum.in.www1.artemis.domain.enumeration.BuildStatus; import de.tum.in.www1.artemis.exception.LocalCIException; -import de.tum.in.www1.artemis.repository.BuildJobRepository; import de.tum.in.www1.artemis.service.connectors.localci.buildagent.BuildAgentDockerService; import de.tum.in.www1.artemis.service.connectors.localci.buildagent.BuildLogsMap; import de.tum.in.www1.artemis.service.connectors.localci.dto.BuildConfig; @@ -45,18 +43,10 @@ class BuildAgentDockerServiceTest extends AbstractSpringIntegrationLocalCILocalV @Autowired private BuildAgentDockerService buildAgentDockerService; - @Autowired - private BuildJobRepository buildJobRepository; - @Autowired @Qualifier("hazelcastInstance") private HazelcastInstance hazelcastInstance; - @AfterEach - void tearDown() { - buildJobRepository.deleteAll(); - } - @Test @Order(2) void testDeleteOldDockerImages() { diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIIntegrationTest.java index 82d4522cf664..2e8fc41172b3 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIIntegrationTest.java @@ -59,7 +59,6 @@ import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exception.VersionControlException; -import de.tum.in.www1.artemis.repository.BuildJobRepository; import de.tum.in.www1.artemis.repository.ProgrammingSubmissionTestRepository; import de.tum.in.www1.artemis.service.BuildLogEntryService; import de.tum.in.www1.artemis.service.ParticipationVcsAccessTokenService; @@ -73,9 +72,6 @@ class LocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { @Autowired private LocalVCServletService localVCServletService; - @Autowired - private BuildJobRepository buildJobRepository; - @Autowired private ProgrammingSubmissionTestRepository programmingSubmissionRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResourceIntegrationTest.java index 00da4ceef09c..d36e8fc760a6 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResourceIntegrationTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; @@ -27,7 +28,6 @@ import de.tum.in.www1.artemis.domain.enumeration.BuildStatus; import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; import de.tum.in.www1.artemis.domain.enumeration.SortingOrder; -import de.tum.in.www1.artemis.repository.BuildJobRepository; import de.tum.in.www1.artemis.service.BuildLogEntryService; import de.tum.in.www1.artemis.service.connectors.localci.buildagent.SharedQueueProcessingService; import de.tum.in.www1.artemis.service.connectors.localci.dto.BuildAgentInformation; @@ -55,6 +55,7 @@ class LocalCIResourceIntegrationTest extends AbstractLocalCILocalVCIntegrationTe protected BuildJob finishedJob3; @Autowired + @Qualifier("hazelcastInstance") private HazelcastInstance hazelcastInstance; @Autowired @@ -69,9 +70,6 @@ class LocalCIResourceIntegrationTest extends AbstractLocalCILocalVCIntegrationTe protected IMap buildAgentInformation; - @Autowired - protected BuildJobRepository buildJobRepository; - @Autowired private PageableSearchUtilService pageableSearchUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java index 4771cfd27156..6564c27d96bc 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.test.context.support.WithMockUser; @@ -66,6 +67,7 @@ class LocalCIServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { private SharedQueueProcessingService sharedQueueProcessingService; @Autowired + @Qualifier("hazelcastInstance") private HazelcastInstance hazelcastInstance; protected IQueue queuedJobs; diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java index 23b4aff66551..f2f41cb0d1f1 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; @@ -33,6 +34,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; @@ -51,7 +54,6 @@ import de.tum.in.www1.artemis.domain.submissionpolicy.LockRepositoryPolicy; import de.tum.in.www1.artemis.domain.submissionpolicy.SubmissionPolicy; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.repository.BuildJobRepository; import de.tum.in.www1.artemis.service.ldap.LdapUserDto; import de.tum.in.www1.artemis.util.LocalRepository; @@ -62,11 +64,10 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class LocalVCLocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { - @Autowired - private ExamUtilService examUtilService; + private static final Logger log = LoggerFactory.getLogger(LocalVCLocalCIIntegrationTest.class); @Autowired - private BuildJobRepository buildJobRepository; + private ExamUtilService examUtilService; // ---- Repository handles ---- private LocalRepository templateRepository; @@ -192,8 +193,8 @@ void testFetchPush_testsRepository() throws Exception { }); // Assert that the build job for the solution was completed before the build job for the template participation has started - var solutionBuildJob = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(solutionParticipation.getId()).get(); - var templateBuildJob = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(templateParticipation.getId()).get(); + var solutionBuildJob = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(solutionParticipation.getId()).orElseThrow(); + var templateBuildJob = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(templateParticipation.getId()).orElseThrow(); assertThat(solutionBuildJob.getBuildCompletionDate()).isBefore(templateBuildJob.getBuildStartDate()); } @@ -946,15 +947,16 @@ void testPriorityRunningExam() throws Exception { Exam exam = examUtilService.addExamWithExerciseGroup(course, true); ExerciseGroup exerciseGroup = exam.getExerciseGroups().getFirst(); + programmingExercise.setCourse(null); programmingExercise.setExerciseGroup(exerciseGroup); - programmingExerciseRepository.save(programmingExercise); + programmingExercise = programmingExerciseRepository.save(programmingExercise); // Exam is running var now = ZonedDateTime.now(); exam.setStartDate(now.minusHours(1)); exam.setEndDate(now.plusHours(1)); exam.setWorkingTime(2 * 60 * 60); - examRepository.save(exam); + exam = examRepository.save(exam); // Create StudentExam. StudentExam studentExam = examUtilService.addStudentExamWithUser(exam, student1); @@ -976,16 +978,22 @@ private void testPriority(String login, int expectedPriority) throws Exception { localVCLocalCITestService.mockTestResults(dockerClient, PARTLY_SUCCESSFUL_TEST_RESULTS_PATH, LOCALCI_WORKING_DIRECTORY + LOCALCI_RESULTS_DIRECTORY); localVCLocalCITestService.testPushSuccessful(assignmentRepository.localGit, login, projectKey1, assignmentRepositorySlug); - await().until(() -> { + log.info("Push done"); + + await().atMost(15, TimeUnit.SECONDS).until(() -> { + log.info("Search for a build job for participation id: {}", studentParticipation.getId()); Optional buildJobOptional = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); - return buildJobOptional.isPresent(); + if (buildJobOptional.isPresent()) { + return true; + } + else { + var allBuildJobs = buildJobRepository.findAll(); + log.info("All found build jobs: {}", allBuildJobs); + return false; + } }); - Optional buildJobOptional = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); - - BuildJob buildJob = buildJobOptional.orElseThrow(); - + BuildJob buildJob = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()).orElseThrow(); assertThat(buildJob.getPriority()).isEqualTo(expectedPriority); - } } diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementService.java b/src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementServiceTest.java similarity index 89% rename from src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementService.java rename to src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementServiceTest.java index b9956458b631..0765e9446ba3 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementService.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementServiceTest.java @@ -4,7 +4,6 @@ import java.time.ZonedDateTime; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -14,7 +13,6 @@ import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.domain.BuildJob; -import de.tum.in.www1.artemis.repository.BuildJobRepository; import de.tum.in.www1.artemis.service.connectors.localci.SharedQueueManagementService; class SharedQueueManagementServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { @@ -22,18 +20,10 @@ class SharedQueueManagementServiceTest extends AbstractSpringIntegrationLocalCIL @Autowired private SharedQueueManagementService sharedQueueManagementService; - @Autowired - private BuildJobRepository buildJobRepository; - @Autowired @Qualifier("hazelcastInstance") private HazelcastInstance hazelcastInstance; - @BeforeEach - void clearBuildJobs() { - buildJobRepository.deleteAll(); - } - @Test void testPushDockerImageCleanupInfo() { From f75e4cf0512b2038582545b6bc5018aa3a7cc5d0 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Tue, 10 Sep 2024 09:42:03 +0200 Subject: [PATCH 08/67] Development: Update server dependencies and remove custom jgit server code --- build.gradle | 78 +++-- docker/mysql.yml | 2 +- docker/postgres.yml | 2 +- docs/admin/database.rst | 4 +- gradle.properties | 9 +- gradle/war.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../buildagent/BuildAgentDockerService.java | 2 +- .../jgit/http/server/AsIsFileFilter.java | 61 ---- .../eclipse/jgit/http/server/FileSender.java | 190 ----------- .../eclipse/jgit/http/server/GitFilter.java | 314 ------------------ .../eclipse/jgit/http/server/GitServlet.java | 192 ----------- .../jgit/http/server/GitSmartHttpTools.java | 286 ---------------- .../jgit/http/server/HttpServerText.java | 52 --- .../jgit/http/server/InfoPacksServlet.java | 46 --- .../jgit/http/server/InfoRefsServlet.java | 59 ---- .../jgit/http/server/IsLocalFilter.java | 55 --- .../jgit/http/server/NoCacheFilter.java | 48 --- .../jgit/http/server/ObjectFileServlet.java | 151 --------- .../http/server/ReceivePackErrorHandler.java | 62 ---- .../jgit/http/server/ReceivePackServlet.java | 192 ----------- .../jgit/http/server/RepositoryFilter.java | 122 ------- .../jgit/http/server/ServletUtils.java | 247 -------------- .../jgit/http/server/SmartOutputStream.java | 102 ------ .../http/server/SmartServiceInfoRefs.java | 198 ----------- .../jgit/http/server/TextFileServlet.java | 54 --- .../http/server/UploadPackErrorHandler.java | 85 ----- .../jgit/http/server/UploadPackServlet.java | 219 ------------ .../jgit/http/server/glue/ErrorServlet.java | 41 --- .../jgit/http/server/glue/MetaFilter.java | 196 ----------- .../jgit/http/server/glue/MetaServlet.java | 110 ------ .../server/glue/NoParameterFilterConfig.java | 57 ---- .../http/server/glue/RegexGroupFilter.java | 71 ---- .../jgit/http/server/glue/RegexPipeline.java | 134 -------- .../jgit/http/server/glue/ServletBinder.java | 35 -- .../http/server/glue/ServletBinderImpl.java | 75 ----- .../jgit/http/server/glue/SuffixPipeline.java | 74 ----- .../jgit/http/server/glue/UrlPipeline.java | 213 ------------ .../jgit/http/server/glue/WrappedRequest.java | 54 --- .../http/server/resolver/AsIsFileService.java | 88 ----- .../resolver/DefaultReceivePackFactory.java | 79 ----- .../resolver/DefaultUploadPackFactory.java | 51 --- .../artemis/statefulsets/artemis-mysql.yml | 2 +- src/test/resources/config/application.yml | 4 +- 44 files changed, 69 insertions(+), 4051 deletions(-) delete mode 100644 src/main/java/org/eclipse/jgit/http/server/AsIsFileFilter.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/FileSender.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/GitFilter.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/GitServlet.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/GitSmartHttpTools.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/HttpServerText.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/InfoPacksServlet.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/InfoRefsServlet.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/IsLocalFilter.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/NoCacheFilter.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/ObjectFileServlet.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/ReceivePackErrorHandler.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/ReceivePackServlet.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/RepositoryFilter.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/ServletUtils.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/SmartOutputStream.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/TextFileServlet.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/UploadPackErrorHandler.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/UploadPackServlet.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/ErrorServlet.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/MetaFilter.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/MetaServlet.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/NoParameterFilterConfig.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/RegexGroupFilter.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/RegexPipeline.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/ServletBinder.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/ServletBinderImpl.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/SuffixPipeline.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/UrlPipeline.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/glue/WrappedRequest.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/resolver/AsIsFileService.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/resolver/DefaultReceivePackFactory.java delete mode 100644 src/main/java/org/eclipse/jgit/http/server/resolver/DefaultUploadPackFactory.java diff --git a/build.gradle b/build.gradle index e53437485601..8b20a431cc56 100644 --- a/build.gradle +++ b/build.gradle @@ -5,13 +5,6 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent import static com.diffplug.spotless.Formatter.NO_FILE_SENTINEL -buildscript { - repositories { - mavenLocal() - mavenCentral() - } -} - plugins { id "checkstyle" id "java" @@ -88,7 +81,7 @@ spotless { removeUnusedImports() trimTrailingWhitespace() - // Wildcard imports can't be resolved by spotless itself. + // Wildcard imports cannot be resolved by spotless itself. // This will require the developer themselves to adhere to best practices. addStep(FormatterStep.createNeverUpToDate("Refuse wildcard imports", new FormatterFunc() { @Override @@ -99,7 +92,7 @@ spotless { @Override String apply(String s, File file) throws Exception { if (s =~ /\nimport .*\*;/) { - throw new AssertionError("Do not use wildcard imports. 'spotlessApply' cannot resolve this issue.\n" + + throw new AssertionError("Do not use wildcard imports. spotlessApply cannot resolve this issue.\n" + "The following file violates this rule: " + file.getName()) } } @@ -210,11 +203,18 @@ configurations { repositories { mavenLocal() mavenCentral() + // required for org.gradle:gradle-tooling-api dependency + maven { + url "https://repo.gradle.org/gradle/libs-releases" + } + // required for org.opensaml:* dependencies maven { - url "https://repo.gradle.org/gradle/libs-releases/" + url "https://build.shibboleth.net/maven/releases" } + // required for latest jgit 7.0.0 dependency + // TODO: remove this when jgit is available in the official maven repository maven { - url "https://build.shibboleth.net/maven/releases/" + url "https://repo.eclipse.org/content/repositories/jgit-releases" } } @@ -264,7 +264,7 @@ dependencies { } } - implementation "org.apache.logging.log4j:log4j-to-slf4j:2.23.1" + implementation "org.apache.logging.log4j:log4j-to-slf4j:2.24.0" // Note: spring-security-lti13 does not work with jakarta yet, so we built our own custom version and declare its transitive dependencies below // implementation "uk.ac.ox.ctl:spring-security-lti13:0.1.11" @@ -275,7 +275,7 @@ dependencies { implementation "org.eclipse.jgit:org.eclipse.jgit.ssh.apache:${jgit_version}" // Note: jgit.htt.server is not compatible with jakarta yet and neither is there a timeline. Hence, we had to add the source files to our repository. // Once the compatibility is given, we can switch back to the maven dependency. - // implementation "org.eclipse.jgit:org.eclipse.jgit.http.server:${jgit_version}" + implementation "org.eclipse.jgit:org.eclipse.jgit.http.server:${jgit_version}" // apache ssh enabled the ssh git operations in LocalVC together with JGit implementation "org.apache.sshd:sshd-core:${sshd_version}" @@ -330,7 +330,7 @@ dependencies { implementation "tech.jhipster:jhipster-framework:${jhipster_dependencies_version}" implementation "org.springframework.boot:spring-boot-starter-cache:${spring_boot_version}" - implementation "io.micrometer:micrometer-registry-prometheus:1.13.3" + implementation "io.micrometer:micrometer-registry-prometheus:1.13.4" implementation "net.logstash.logback:logstash-logback-encoder:8.0" // Defines low-level streaming API, and includes JSON-specific implementations @@ -390,7 +390,7 @@ dependencies { implementation "org.springframework.cloud:spring-cloud-starter-config:4.1.3" implementation "org.springframework.cloud:spring-cloud-commons:4.1.4" - implementation "io.netty:netty-all:4.1.112.Final" + implementation "io.netty:netty-all:4.1.113.Final" implementation "io.projectreactor.netty:reactor-netty:1.1.22" implementation "org.springframework:spring-messaging:6.1.12" implementation "org.springframework.retry:spring-retry:2.0.8" @@ -451,6 +451,38 @@ dependencies { implementation "com.google.errorprone:error_prone_annotations:2.31.0" + // NOTE: we want to keep the same unique version for all configurations, implementation and annotationProcessor + implementation("net.bytebuddy:byte-buddy") { + version { + strictly byte_buddy_version + } + } + annotationProcessor("net.bytebuddy:byte-buddy") { + version { + strictly byte_buddy_version + } + } + liquibase("net.bytebuddy:byte-buddy") { + version { + strictly byte_buddy_version + } + } + implementation("net.bytebuddy:byte-buddy-agent") { + version { + strictly byte_buddy_version + } + } + annotationProcessor("net.bytebuddy:byte-buddy-agent") { + version { + strictly byte_buddy_version + } + } + liquibase("net.bytebuddy:byte-buddy-agent") { + version { + strictly byte_buddy_version + } + } + annotationProcessor "org.hibernate:hibernate-jpamodelgen:${hibernate_version}" annotationProcessor("org.glassfish.jaxb:jaxb-runtime:${jaxb_runtime_version}") { exclude group: "jakarta.ws.rs", module: "jsr311-api" @@ -482,7 +514,7 @@ dependencies { testImplementation "io.github.classgraph:classgraph:4.8.175" testImplementation "org.awaitility:awaitility:4.2.2" testImplementation "org.apache.maven.shared:maven-invoker:3.3.0" - testImplementation "org.gradle:gradle-tooling-api:8.10" + testImplementation "org.gradle:gradle-tooling-api:8.10.1" testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.0" testImplementation "com.opencsv:opencsv:5.9" testImplementation("io.zonky.test:embedded-database-spring-test:2.5.1") { @@ -493,24 +525,22 @@ dependencies { testImplementation("org.skyscreamer:jsonassert:1.5.3") { exclude module: "android-json" } - testImplementation("net.bytebuddy:byte-buddy") { - version { - strictly "1.15.1" - } - } + // cannot update due to "Syntax error in SQL statement "WITH ids_to_delete" // testImplementation "com.h2database:h2:2.3.230" testImplementation "com.h2database:h2:2.2.224" // Lightweight JSON library needed for the internals of the MockRestServiceServer testImplementation "org.json:json:20240303" + + testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.11.0" } ext["junit-jupiter.version"] = junit_version dependencyManagement { imports { - mavenBom "io.zonky.test.postgres:embedded-postgres-binaries-bom:16.2.0" + mavenBom "io.zonky.test.postgres:embedded-postgres-binaries-bom:16.4.0" } } @@ -519,7 +549,7 @@ tasks.register("cleanResources", Delete) { } tasks.withType(JavaCompile).configureEach { - options.compilerArgs << '-Xlint:deprecation' + options.compilerArgs << "-Xlint:deprecation" } // Taken from here: https://stackoverflow.com/questions/3963708/gradle-how-to-display-test-results-in-the-console-in-real-time @@ -567,7 +597,7 @@ tasks.withType(Test).configureEach { } wrapper { - gradleVersion = "8.10" + gradleVersion = "8.10.1" } tasks.register("stage") { diff --git a/docker/mysql.yml b/docker/mysql.yml index 1a644537e195..741440124f22 100644 --- a/docker/mysql.yml +++ b/docker/mysql.yml @@ -5,7 +5,7 @@ services: mysql: container_name: artemis-mysql - image: docker.io/library/mysql:8.4.0 + image: docker.io/library/mysql:9.0.1 pull_policy: if_not_present volumes: - artemis-mysql-data:/var/lib/mysql diff --git a/docker/postgres.yml b/docker/postgres.yml index bfef3d3818b8..c96b36d0ca18 100644 --- a/docker/postgres.yml +++ b/docker/postgres.yml @@ -5,7 +5,7 @@ services: postgres: container_name: artemis-postgres - image: docker.io/library/postgres:16.3-alpine + image: docker.io/library/postgres:16.4-alpine pull_policy: if_not_present user: postgres command: ["postgres", "-c", "max_connections=10000"] diff --git a/docs/admin/database.rst b/docs/admin/database.rst index 9bf00ec5d2a6..76d09c952358 100644 --- a/docs/admin/database.rst +++ b/docs/admin/database.rst @@ -42,7 +42,7 @@ Migrating MySQL Data to PostgreSQL --- services: mysql: - image: docker.io/library/mysql:8.4.0 + image: docker.io/library/mysql:9.0.1 environment: - MYSQL_DATABASE=Artemis - MYSQL_ALLOW_EMPTY_PASSWORD=yes @@ -59,7 +59,7 @@ Migrating MySQL Data to PostgreSQL - db-migration postgres: - image: docker.io/library/postgres:16.3 + image: docker.io/library/postgres:16.4 environment: - POSTGRES_USER=root - POSTGRES_DB=Artemis diff --git a/gradle.properties b/gradle.properties index 80dbe5279b3e..fb4f6273eefe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,16 +19,17 @@ hazelcast_version=5.5.0 junit_version=5.10.2 mockito_version=5.13.0 fasterxml_version=2.17.2 -jgit_version=6.10.0.202406032230-r +jgit_version=7.0.0.202409031743-r sshd_version=2.13.2 checkstyle_version=10.18.1 jplag_version=5.1.0 slf4j_version=2.0.16 sentry_version=7.14.0 -liquibase_version=4.29.1 +liquibase_version=4.29.2 docker_java_version=3.4.0 -logback_version=1.5.7 -java_parser_version=3.26.1 +logback_version=1.5.8 +java_parser_version=3.26.2 +byte_buddy_version=1.15.1 # gradle plugin version gradle_node_plugin_version=7.0.2 diff --git a/gradle/war.gradle b/gradle/war.gradle index 5fd2d0920a07..a38ec443d813 100644 --- a/gradle/war.gradle +++ b/gradle/war.gradle @@ -13,7 +13,7 @@ bootWar { // Command for production build: ./gradlew -Pprod -Pwar clean bootWar war { - webAppDirName = file("build/resources/main/static/") + webAppDirectory = file("build/resources/main/static/") includes = ["WEB-INF/**", "META-INF/**"] webXml = file("${project.rootDir}/src/main/webapp/WEB-INF/web.xml") enabled = true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b4155759..0aaefbcaf0f1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildAgentDockerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildAgentDockerService.java index 09df86cb270a..dd2885a7827d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildAgentDockerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildAgentDockerService.java @@ -130,7 +130,7 @@ public void cleanUpContainers() { .filter(container -> container.getNames()[0].startsWith("/" + buildContainerPrefix)).toList(); } catch (Exception ex) { - log.error("Make sure Docker is running! Error while listing containers for cleanup: {}", ex.getMessage(), ex); + log.error("Make sure Docker is running and configured properly! Error while listing containers for cleanup: {}", ex.getMessage(), ex); return; } finally { diff --git a/src/main/java/org/eclipse/jgit/http/server/AsIsFileFilter.java b/src/main/java/org/eclipse/jgit/http/server/AsIsFileFilter.java deleted file mode 100644 index b1b6935bceef..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/AsIsFileFilter.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import org.eclipse.jgit.http.server.resolver.AsIsFileService; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; -import static org.eclipse.jgit.http.server.ServletUtils.getRepository; - -class AsIsFileFilter implements Filter { - - private final AsIsFileService asIs; - - AsIsFileFilter(AsIsFileService getAnyFile) { - this.asIs = getAnyFile; - } - - @Override - public void init(FilterConfig config) throws ServletException { - // Do nothing. - } - - @Override - public void destroy() { - // Do nothing. - } - - @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - HttpServletRequest req = (HttpServletRequest) request; - HttpServletResponse res = (HttpServletResponse) response; - try { - final Repository db = getRepository(request); - asIs.access(req, db); - chain.doFilter(request, response); - } - catch (ServiceNotAuthorizedException e) { - res.sendError(SC_UNAUTHORIZED, e.getMessage()); - } - catch (ServiceNotEnabledException e) { - res.sendError(SC_FORBIDDEN, e.getMessage()); - } - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/FileSender.java b/src/main/java/org/eclipse/jgit/http/server/FileSender.java deleted file mode 100644 index 53f0cf0c02dc..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/FileSender.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.EOFException; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStream; -import java.io.RandomAccessFile; -import java.text.MessageFormat; -import java.time.Instant; -import java.util.Enumeration; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.util.FS; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT; -import static jakarta.servlet.http.HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE; -import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_RANGES; -import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_LENGTH; -import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_RANGE; -import static org.eclipse.jgit.util.HttpSupport.HDR_IF_RANGE; -import static org.eclipse.jgit.util.HttpSupport.HDR_RANGE; - -/** - * Dumps a file over HTTP GET (or its information via HEAD). - *

    - * Supports a single byte range requested via {@code Range} HTTP header. This - * feature supports a dumb client to resume download of a larger object file. - */ -final class FileSender { - - private final File path; - - private final RandomAccessFile source; - - private final Instant lastModified; - - private final long fileLen; - - private long pos; - - private long end; - - FileSender(File path) throws FileNotFoundException { - this.path = path; - this.source = new RandomAccessFile(path, "r"); - - try { - this.lastModified = FS.DETECTED.lastModifiedInstant(path); - this.fileLen = source.getChannel().size(); - this.end = fileLen; - } - catch (IOException e) { - try { - source.close(); - } - catch (IOException closeError) { - // Ignore any error closing the stream. - } - - final FileNotFoundException r; - r = new FileNotFoundException(MessageFormat.format(HttpServerText.get().cannotGetLengthOf, path)); - r.initCause(e); - throw r; - } - } - - void close() { - try { - source.close(); - } - catch (IOException e) { - // Ignore close errors on a read-only stream. - } - } - - Instant getLastModified() { - return lastModified; - } - - String getTailChecksum() throws IOException { - final int n = 20; - final byte[] buf = new byte[n]; - source.seek(fileLen - n); - source.readFully(buf, 0, n); - return ObjectId.fromRaw(buf).getName(); - } - - void serve(final HttpServletRequest req, final HttpServletResponse rsp, final boolean sendBody) throws IOException { - if (!initRangeRequest(req, rsp)) { - rsp.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE); - return; - } - - rsp.setHeader(HDR_ACCEPT_RANGES, "bytes"); - rsp.setHeader(HDR_CONTENT_LENGTH, Long.toString(end - pos)); - - if (sendBody) { - try (OutputStream out = rsp.getOutputStream()) { - final byte[] buf = new byte[4096]; - source.seek(pos); - while (pos < end) { - final int r = (int) Math.min(buf.length, end - pos); - final int n = source.read(buf, 0, r); - if (n < 0) { - throw new EOFException(MessageFormat.format(HttpServerText.get().unexpectedeOFOn, path)); - } - out.write(buf, 0, n); - pos += n; - } - out.flush(); - } - } - } - - private boolean initRangeRequest(final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { - final Enumeration rangeHeaders = getRange(req); - if (!rangeHeaders.hasMoreElements()) { - // No range headers, the request is fine. - return true; - } - - final String range = rangeHeaders.nextElement(); - if (rangeHeaders.hasMoreElements()) { - // To simplify the code we support only one range. - return false; - } - - final int eq = range.indexOf('='); - final int dash = range.indexOf('-'); - if (eq < 0 || dash < 0 || !range.startsWith("bytes=")) { - return false; - } - - final String ifRange = req.getHeader(HDR_IF_RANGE); - if (ifRange != null && !getTailChecksum().equals(ifRange)) { - // If the client asked us to verify the ETag and its not - // what they expected we need to send the entire content. - return true; - } - - try { - if (eq + 1 == dash) { - // "bytes=-500" means last 500 bytes - pos = Long.parseLong(range.substring(dash + 1)); - pos = fileLen - pos; - } - else { - // "bytes=500-" (position 500 to end) - // "bytes=500-1000" (position 500 to 1000) - pos = Long.parseLong(range.substring(eq + 1, dash)); - if (dash < range.length() - 1) { - end = Long.parseLong(range.substring(dash + 1)); - end++; // range was inclusive, want exclusive - } - } - } - catch (NumberFormatException e) { - // We probably hit here because of a non-digit such as - // "," appearing at the end of the first range telling - // us there is a second range following. To simplify - // the code we support only one range. - return false; - } - - if (end > fileLen) { - end = fileLen; - } - if (pos >= end) { - return false; - } - - rsp.setStatus(SC_PARTIAL_CONTENT); - rsp.setHeader(HDR_CONTENT_RANGE, "bytes " + pos + "-" + (end - 1) + "/" + fileLen); - source.seek(pos); - return true; - } - - private static Enumeration getRange(HttpServletRequest req) { - return req.getHeaders(HDR_RANGE); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/GitFilter.java b/src/main/java/org/eclipse/jgit/http/server/GitFilter.java deleted file mode 100644 index 27c96379419f..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/GitFilter.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.File; -import java.text.MessageFormat; -import java.util.LinkedList; -import java.util.List; -import org.eclipse.jgit.http.server.glue.ErrorServlet; -import org.eclipse.jgit.http.server.glue.MetaFilter; -import org.eclipse.jgit.http.server.glue.RegexGroupFilter; -import org.eclipse.jgit.http.server.glue.ServletBinder; -import org.eclipse.jgit.http.server.resolver.AsIsFileService; -import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory; -import org.eclipse.jgit.http.server.resolver.DefaultUploadPackFactory; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.transport.resolver.FileResolver; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.RepositoryResolver; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; -import org.eclipse.jgit.util.StringUtils; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -/** - * Handles Git repository access over HTTP. - *

    - * Applications embedding this filter should map a directory path within the - * application to this filter. For a servlet version, see - * {@link org.eclipse.jgit.http.server.GitServlet}. - *

    - * Applications may wish to add additional repository action URLs to this - * servlet by taking advantage of its extension from - * {@link org.eclipse.jgit.http.server.glue.MetaFilter}. Callers may register - * their own URL suffix translations through {@link #serve(String)}, or their - * regex translations through {@link #serveRegex(String)}. Each translation - * should contain a complete filter pipeline which ends with the HttpServlet - * that should handle the requested action. - */ -public class GitFilter extends MetaFilter { - - private volatile boolean initialized; - - private RepositoryResolver resolver; - - private AsIsFileService asIs = new AsIsFileService(); - - private UploadPackFactory uploadPackFactory = new DefaultUploadPackFactory(); - - private UploadPackErrorHandler uploadPackErrorHandler; - - private ReceivePackFactory receivePackFactory = new DefaultReceivePackFactory(); - - private ReceivePackErrorHandler receivePackErrorHandler; - - private final List uploadPackFilters = new LinkedList<>(); - - private final List receivePackFilters = new LinkedList<>(); - - /** - * New servlet that will load its base directory from {@code web.xml}. - *

    - * The required parameter {@code base-path} must be configured to point to - * the local filesystem directory where all served Git repositories reside. - */ - public GitFilter() { - // Initialized above by field declarations. - } - - /** - * New servlet configured with a specific resolver. - * - * @param resolver - * the resolver to use when matching URL to Git repository. If - * null the {@code base-path} parameter will be looked for in the - * parameter table during init, which usually comes from the - * {@code web.xml} file of the web application. - */ - public void setRepositoryResolver(RepositoryResolver resolver) { - assertNotInitialized(); - this.resolver = resolver; - } - - /** - * Set AsIsFileService - * - * @param f - * the filter to validate direct access to repository files - * through a dumb client. If {@code null} then dumb client - * support is completely disabled. - */ - public void setAsIsFileService(AsIsFileService f) { - assertNotInitialized(); - this.asIs = f != null ? f : AsIsFileService.DISABLED; - } - - /** - * Set upload-pack factory - * - * @param f - * the factory to construct and configure an - * {@link org.eclipse.jgit.transport.UploadPack} session when a - * fetch or clone is requested by a client. - */ - @SuppressWarnings("unchecked") public void setUploadPackFactory(UploadPackFactory f) { - assertNotInitialized(); - this.uploadPackFactory = f != null ? f : (UploadPackFactory) UploadPackFactory.DISABLED; - } - - /** - * Set a custom error handler for git-upload-pack. - * - * @param h - * A custom error handler for git-upload-pack. - * @since 5.6 - */ - public void setUploadPackErrorHandler(UploadPackErrorHandler h) { - assertNotInitialized(); - this.uploadPackErrorHandler = h; - } - - /** - * Add upload-pack filter - * - * @param filter - * filter to apply before any of the UploadPack operations. The - * UploadPack instance is available in the request attribute - * {@link org.eclipse.jgit.http.server.ServletUtils#ATTRIBUTE_HANDLER}. - */ - public void addUploadPackFilter(Filter filter) { - assertNotInitialized(); - uploadPackFilters.add(filter); - } - - /** - * Set the receive-pack factory - * - * @param f - * the factory to construct and configure a - * {@link org.eclipse.jgit.transport.ReceivePack} session when a - * push is requested by a client. - */ - @SuppressWarnings("unchecked") public void setReceivePackFactory(ReceivePackFactory f) { - assertNotInitialized(); - this.receivePackFactory = f != null ? f : (ReceivePackFactory) ReceivePackFactory.DISABLED; - } - - /** - * Set a custom error handler for git-receive-pack. - * - * @param h - * A custom error handler for git-receive-pack. - * @since 5.7 - */ - public void setReceivePackErrorHandler(ReceivePackErrorHandler h) { - assertNotInitialized(); - this.receivePackErrorHandler = h; - } - - /** - * Add receive-pack filter - * - * @param filter - * filter to apply before any of the ReceivePack operations. The - * ReceivePack instance is available in the request attribute - * {@link org.eclipse.jgit.http.server.ServletUtils#ATTRIBUTE_HANDLER}. - */ - public void addReceivePackFilter(Filter filter) { - assertNotInitialized(); - receivePackFilters.add(filter); - } - - private void assertNotInitialized() { - if (initialized) { - throw new IllegalStateException(HttpServerText.get().alreadyInitializedByContainer); - } - } - - @Override public void init(FilterConfig filterConfig) throws ServletException { - super.init(filterConfig); - - if (resolver == null) { - File root = getFile(filterConfig, "base-path"); - boolean exportAll = getBoolean(filterConfig, "export-all"); - setRepositoryResolver(new FileResolver<>(root, exportAll)); - } - - initialized = true; - - if (uploadPackFactory != UploadPackFactory.DISABLED) { - ServletBinder b = serve("*/" + GitSmartHttpTools.UPLOAD_PACK); - b = b.through(new UploadPackServlet.Factory(uploadPackFactory)); - for (Filter f : uploadPackFilters) { - b = b.through(f); - } - b.with(new UploadPackServlet(uploadPackErrorHandler)); - } - - if (receivePackFactory != ReceivePackFactory.DISABLED) { - ServletBinder b = serve("*/" + GitSmartHttpTools.RECEIVE_PACK); - b = b.through(new ReceivePackServlet.Factory(receivePackFactory)); - for (Filter f : receivePackFilters) { - b = b.through(f); - } - b.with(new ReceivePackServlet(receivePackErrorHandler)); - } - - ServletBinder refs = serve("*/" + Constants.INFO_REFS); - if (uploadPackFactory != UploadPackFactory.DISABLED) { - refs = refs.through(new UploadPackServlet.InfoRefs(uploadPackFactory, uploadPackFilters)); - } - if (receivePackFactory != ReceivePackFactory.DISABLED) { - refs = refs.through(new ReceivePackServlet.InfoRefs(receivePackFactory, receivePackFilters)); - } - if (asIs != AsIsFileService.DISABLED) { - refs = refs.through(new IsLocalFilter()); - refs = refs.through(new AsIsFileFilter(asIs)); - refs.with(new InfoRefsServlet()); - } - else { - refs.with(new ErrorServlet(HttpServletResponse.SC_NOT_ACCEPTABLE)); - } - - if (asIs != AsIsFileService.DISABLED) { - final IsLocalFilter mustBeLocal = new IsLocalFilter(); - final AsIsFileFilter enabled = new AsIsFileFilter(asIs); - - serve("*/" + Constants.HEAD)// - .through(mustBeLocal)// - .through(enabled)// - .with(new TextFileServlet(Constants.HEAD)); - - final String info_alternates = Constants.OBJECTS + "/" + Constants.INFO_ALTERNATES; - serve("*/" + info_alternates)// - .through(mustBeLocal)// - .through(enabled)// - .with(new TextFileServlet(info_alternates)); - - final String http_alternates = Constants.OBJECTS + "/" + Constants.INFO_HTTP_ALTERNATES; - serve("*/" + http_alternates)// - .through(mustBeLocal)// - .through(enabled)// - .with(new TextFileServlet(http_alternates)); - - serve("*/objects/info/packs")// - .through(mustBeLocal)// - .through(enabled)// - .with(new InfoPacksServlet()); - - serveRegex("^/(.*)/objects/([0-9a-f]{2}/[0-9a-f]{38})$")// - .through(mustBeLocal)// - .through(enabled)// - .through(new RegexGroupFilter(2))// - .with(new ObjectFileServlet.Loose()); - - serveRegex("^/(.*)/objects/(pack/pack-[0-9a-f]{40}\\.pack)$")// - .through(mustBeLocal)// - .through(enabled)// - .through(new RegexGroupFilter(2))// - .with(new ObjectFileServlet.Pack()); - - serveRegex("^/(.*)/objects/(pack/pack-[0-9a-f]{40}\\.idx)$")// - .through(mustBeLocal)// - .through(enabled)// - .through(new RegexGroupFilter(2))// - .with(new ObjectFileServlet.PackIdx()); - } - } - - private static File getFile(FilterConfig cfg, String param) throws ServletException { - String initParameter = cfg.getInitParameter(param); - if (StringUtils.isEmptyOrNull(initParameter)) { - throw new ServletException(MessageFormat.format(HttpServerText.get().parameterNotSet, param)); - } - - File path = new File(initParameter); - if (!path.exists()) { - throw new ServletException(MessageFormat.format(HttpServerText.get().pathForParamNotFound, path, param)); - } - return path; - } - - private static boolean getBoolean(FilterConfig cfg, String param) throws ServletException { - String initParameter = cfg.getInitParameter(param); - if (initParameter == null) { - return false; - } - try { - return StringUtils.toBoolean(initParameter); - } - catch (IllegalArgumentException err) { - throw new ServletException(MessageFormat.format(HttpServerText.get().invalidBoolean, param, initParameter), err); - } - } - - @Override - protected ServletBinder register(ServletBinder binder) { - if (resolver == null) { - throw new IllegalStateException(HttpServerText.get().noResolverAvailable); - } - binder = binder.through(new NoCacheFilter()); - binder = binder.through(new RepositoryFilter(resolver)); - return binder; - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/GitServlet.java b/src/main/java/org/eclipse/jgit/http/server/GitServlet.java deleted file mode 100644 index 2848bd3312d9..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/GitServlet.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.Serial; -import java.util.Enumeration; -import org.eclipse.jgit.http.server.glue.MetaServlet; -import org.eclipse.jgit.http.server.resolver.AsIsFileService; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.RepositoryResolver; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; - -/** - * Handles Git repository access over HTTP. - *

    - * Applications embedding this servlet should map a directory path within the - * application to this servlet, for example: - * - *

    - *   <servlet>
    - *     <servlet-name>GitServlet</servlet-name>
    - *     <servlet-class>org.eclipse.jgit.http.server.GitServlet</servlet-class>
    - *     <init-param>
    - *       <param-name>base-path</param-name>
    - *       <param-value>/var/srv/git</param-value>
    - *     </init-param>
    - *     <init-param>
    - *       <param-name>export-all</param-name>
    - *       <param-value>0</param-value>
    - *     </init-param>
    - * </servlet>
    - *   <servlet-mapping>
    - *     <servlet-name>GitServlet</servlet-name>
    - *     <url-pattern>/git/*</url-pattern>
    - *   </servlet-mapping>
    - * 
    - * - *

    - * Applications may wish to add additional repository action URLs to this - * servlet by taking advantage of its extension from - * {@link org.eclipse.jgit.http.server.glue.MetaServlet}. Callers may register - * their own URL suffix translations through {@link #serve(String)}, or their - * regex translations through {@link #serveRegex(String)}. Each translation - * should contain a complete filter pipeline which ends with the HttpServlet - * that should handle the requested action. - */ -public class GitServlet extends MetaServlet { - - @Serial - private static final long serialVersionUID = 1L; - - private final GitFilter gitFilter; - - /** - * New servlet that will load its base directory from {@code web.xml}. - *

    - * The required parameter {@code base-path} must be configured to point to - * the local filesystem directory where all served Git repositories reside. - */ - public GitServlet() { - super(new GitFilter()); - gitFilter = (GitFilter) getDelegateFilter(); - } - - /** - * New servlet configured with a specific resolver. - * - * @param resolver - * the resolver to use when matching URL to Git repository. If - * null the {@code base-path} parameter will be looked for in the - * parameter table during init, which usually comes from the - * {@code web.xml} file of the web application. - */ - public void setRepositoryResolver(RepositoryResolver resolver) { - gitFilter.setRepositoryResolver(resolver); - } - - /** - * Set AsIsFileService - * - * @param f - * the filter to validate direct access to repository files - * through a dumb client. If {@code null} then dumb client - * support is completely disabled. - */ - public void setAsIsFileService(AsIsFileService f) { - gitFilter.setAsIsFileService(f); - } - - /** - * Set upload-pack factory - * - * @param f - * the factory to construct and configure an - * {@link org.eclipse.jgit.transport.UploadPack} session when a - * fetch or clone is requested by a client. - */ - public void setUploadPackFactory(UploadPackFactory f) { - gitFilter.setUploadPackFactory(f); - } - - /** - * Set a custom error handler for git-upload-pack. - * - * @param h - * A custom error handler for git-upload-pack. - * @since 5.9.1 - */ - public void setUploadPackErrorHandler(UploadPackErrorHandler h) { - gitFilter.setUploadPackErrorHandler(h); - } - - /** - * Add upload-pack filter - * - * @param filter - * filter to apply before any of the UploadPack operations. The - * UploadPack instance is available in the request attribute - * {@link org.eclipse.jgit.http.server.ServletUtils#ATTRIBUTE_HANDLER}. - */ - public void addUploadPackFilter(Filter filter) { - gitFilter.addUploadPackFilter(filter); - } - - /** - * Set receive-pack factory - * - * @param f - * the factory to construct and configure a - * {@link org.eclipse.jgit.transport.ReceivePack} session when a - * push is requested by a client. - */ - public void setReceivePackFactory(ReceivePackFactory f) { - gitFilter.setReceivePackFactory(f); - } - - /** - * Set a custom error handler for git-receive-pack. - * - * @param h - * A custom error handler for git-receive-pack. - * @since 5.9.1 - */ - public void setReceivePackErrorHandler(ReceivePackErrorHandler h) { - gitFilter.setReceivePackErrorHandler(h); - } - - /** - * Add receive-pack filter - * - * @param filter - * filter to apply before any of the ReceivePack operations. The - * ReceivePack instance is available in the request attribute - * {@link org.eclipse.jgit.http.server.ServletUtils#ATTRIBUTE_HANDLER}. - */ - public void addReceivePackFilter(Filter filter) { - gitFilter.addReceivePackFilter(filter); - } - - @Override public void init(ServletConfig config) throws ServletException { - gitFilter.init(new FilterConfig() { - - @Override public String getFilterName() { - return gitFilter.getClass().getName(); - } - - @Override public String getInitParameter(String name) { - return config.getInitParameter(name); - } - - @Override public Enumeration getInitParameterNames() { - return config.getInitParameterNames(); - } - - @Override public ServletContext getServletContext() { - return config.getServletContext(); - } - }); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/GitSmartHttpTools.java b/src/main/java/org/eclipse/jgit/http/server/GitSmartHttpTools.java deleted file mode 100644 index 2579fcadfa8b..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/GitSmartHttpTools.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (C) 2011, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.List; -import org.eclipse.jgit.internal.transport.parser.FirstCommand; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.transport.PacketLineIn; -import org.eclipse.jgit.transport.PacketLineOut; -import org.eclipse.jgit.transport.ReceivePack; -import org.eclipse.jgit.transport.RequestNotYetReadException; -import org.eclipse.jgit.transport.SideBandOutputStream; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jgit.util.StringUtils; -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; -import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; -import static org.eclipse.jgit.http.server.ServletUtils.ATTRIBUTE_HANDLER; -import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BAND_64K; -import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR; -import static org.eclipse.jgit.transport.SideBandOutputStream.SMALL_BUF; - -/** - * Utility functions for handling the Git-over-HTTP protocol. - */ -public class GitSmartHttpTools { - - private static final String INFO_REFS = Constants.INFO_REFS; - - /** Name of the git-upload-pack service. */ - public static final String UPLOAD_PACK = "git-upload-pack"; - - /** Name of the git-receive-pack service. */ - public static final String RECEIVE_PACK = "git-receive-pack"; - - /** Content type supplied by the client to the /git-upload-pack handler. */ - public static final String UPLOAD_PACK_REQUEST_TYPE = "application/x-git-upload-pack-request"; - - /** Content type returned from the /git-upload-pack handler. */ - public static final String UPLOAD_PACK_RESULT_TYPE = "application/x-git-upload-pack-result"; - - /** Content type supplied by the client to the /git-receive-pack handler. */ - public static final String RECEIVE_PACK_REQUEST_TYPE = "application/x-git-receive-pack-request"; - - /** Content type returned from the /git-receive-pack handler. */ - public static final String RECEIVE_PACK_RESULT_TYPE = "application/x-git-receive-pack-result"; - - /** Git service names accepted by the /info/refs?service= handler. */ - public static final List VALID_SERVICES = List.of(UPLOAD_PACK, RECEIVE_PACK); - - private static final String INFO_REFS_PATH = "/" + INFO_REFS; - - private static final String UPLOAD_PACK_PATH = "/" + UPLOAD_PACK; - - private static final String RECEIVE_PACK_PATH = "/" + RECEIVE_PACK; - - /** - * Send an error to the Git client or browser. - *

    - * Server implementors may use this method to send customized error messages - * to a Git protocol client using an HTTP 200 OK response with the error - * embedded in the payload. If the request was not issued by a Git client, - * an HTTP response code is returned instead. - * - * @param req - * current request. - * @param res - * current response. - * @param httpStatus - * HTTP status code to set if the client is not a Git client. - * @throws IOException - * the response cannot be sent. - */ - public static void sendError(HttpServletRequest req, HttpServletResponse res, int httpStatus) throws IOException { - sendError(req, res, httpStatus, null); - } - - /** - * Send an error to the Git client or browser. - *

    - * Server implementors may use this method to send customized error messages - * to a Git protocol client using an HTTP 200 OK response with the error - * embedded in the payload. If the request was not issued by a Git client, - * an HTTP response code is returned instead. - *

    - * This method may only be called before handing off the request to - * {@link org.eclipse.jgit.transport.UploadPack#upload(java.io.InputStream, OutputStream, OutputStream)} - * or - * {@link org.eclipse.jgit.transport.ReceivePack#receive(java.io.InputStream, OutputStream, OutputStream)}. - * - * @param req - * current request. - * @param res - * current response. - * @param httpStatus - * HTTP status code to set if the client is not a Git client. - * @param textForGit - * plain text message to display on the user's console. This is - * shown only if the client is likely to be a Git client. If null - * or the empty string a default text is chosen based on the HTTP - * response code. - * @throws IOException - * the response cannot be sent. - */ - public static void sendError(HttpServletRequest req, HttpServletResponse res, int httpStatus, String textForGit) throws IOException { - if (StringUtils.isEmptyOrNull(textForGit)) { - switch (httpStatus) { - case SC_FORBIDDEN: - textForGit = HttpServerText.get().repositoryAccessForbidden; - break; - case SC_NOT_FOUND: - textForGit = HttpServerText.get().repositoryNotFound; - break; - case SC_INTERNAL_SERVER_ERROR: - textForGit = HttpServerText.get().internalServerError; - break; - default: - textForGit = "HTTP " + httpStatus; - break; - } - } - - if (isInfoRefs(req)) { - sendInfoRefsError(req, res, textForGit, httpStatus); - } - else if (isUploadPack(req)) { - sendUploadPackError(req, res, textForGit, httpStatus); - } - else if (isReceivePack(req)) { - sendReceivePackError(req, res, textForGit, httpStatus); - } - else { - if (httpStatus < 400) { - ServletUtils.consumeRequestBody(req); - } - res.sendError(httpStatus, textForGit); - } - } - - private static void sendInfoRefsError(HttpServletRequest req, HttpServletResponse res, String textForGit, int httpStatus) throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(128); - PacketLineOut pck = new PacketLineOut(buf); - String svc = req.getParameter("service"); - pck.writeString("# service=" + svc + "\n"); - pck.end(); - pck.writeString("ERR " + textForGit); - send(req, res, infoRefsResultType(svc), buf.toByteArray(), httpStatus); - } - - private static void sendUploadPackError(HttpServletRequest req, HttpServletResponse res, String textForGit, int httpStatus) throws IOException { - // Do not use sideband. Sideband is acceptable only while packfile is - // being sent. Other places, like acknowledgement section, do not - // support sideband. Use an error packet. - ByteArrayOutputStream buf = new ByteArrayOutputStream(128); - PacketLineOut pckOut = new PacketLineOut(buf); - writePacket(pckOut, textForGit); - send(req, res, UPLOAD_PACK_RESULT_TYPE, buf.toByteArray(), httpStatus); - } - - private static void sendReceivePackError(HttpServletRequest req, HttpServletResponse res, String textForGit, int httpStatus) throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(128); - PacketLineOut pckOut = new PacketLineOut(buf); - - boolean sideband; - ReceivePack rp = (ReceivePack) req.getAttribute(ATTRIBUTE_HANDLER); - if (rp != null) { - try { - sideband = rp.isSideBand(); - } - catch (RequestNotYetReadException e) { - sideband = isReceivePackSideBand(req); - } - } - else { - sideband = isReceivePackSideBand(req); - } - - if (sideband) { - writeSideBand(buf, textForGit); - } - else { - writePacket(pckOut, textForGit); - } - send(req, res, RECEIVE_PACK_RESULT_TYPE, buf.toByteArray(), httpStatus); - } - - private static boolean isReceivePackSideBand(HttpServletRequest req) { - try { - // The client may be in a state where they have sent the sideband - // capability and are expecting a response in the sideband, but we might - // not have a ReceivePack, or it might not have read any of the request. - // So, cheat and read the first line. - String line = new PacketLineIn(req.getInputStream()).readString(); - FirstCommand parsed = FirstCommand.fromLine(line); - return parsed.getCapabilities().containsKey(CAPABILITY_SIDE_BAND_64K); - } - catch (IOException e) { - // Probably the connection is closed and a subsequent write will fail, but - // try it just in case. - return false; - } - } - - private static void writeSideBand(OutputStream out, String textForGit) throws IOException { - try (OutputStream msg = new SideBandOutputStream(CH_ERROR, SMALL_BUF, out)) { - msg.write(Constants.encode("error: " + textForGit)); - msg.flush(); - } - } - - private static void writePacket(PacketLineOut pckOut, String textForGit) throws IOException { - pckOut.writeString("ERR " + textForGit); - } - - private static void send(HttpServletRequest req, HttpServletResponse res, String type, byte[] buf, int httpStatus) throws IOException { - ServletUtils.consumeRequestBody(req); - res.setStatus(httpStatus); - res.setContentType(type); - res.setContentLength(buf.length); - try (OutputStream os = res.getOutputStream()) { - os.write(buf); - } - } - - static String infoRefsResultType(String svc) { - return "application/x-" + svc + "-advertisement"; - } - - /** - * Check if the HTTP request was for the /info/refs?service= Git handler. - * - * @param req - * current request. - * @return true if the request is for the /info/refs service. - */ - public static boolean isInfoRefs(HttpServletRequest req) { - return req.getRequestURI().endsWith(INFO_REFS_PATH) && VALID_SERVICES.contains(req.getParameter("service")); - } - - /** - * Check if the HTTP request path ends with the /git-upload-pack handler. - * - * @param pathOrUri - * path or URI of the request. - * @return true if the request is for the /git-upload-pack handler. - */ - public static boolean isUploadPack(String pathOrUri) { - return pathOrUri != null && pathOrUri.endsWith(UPLOAD_PACK_PATH); - } - - /** - * Check if the HTTP request was for the /git-upload-pack Git handler. - * - * @param req - * current request. - * @return true if the request is for the /git-upload-pack handler. - */ - public static boolean isUploadPack(HttpServletRequest req) { - return isUploadPack(req.getRequestURI()) && UPLOAD_PACK_REQUEST_TYPE.equals(req.getContentType()); - } - - /** - * Check if the HTTP request was for the /git-receive-pack Git handler. - * - * @param req - * current request. - * @return true if the request is for the /git-receive-pack handler. - */ - public static boolean isReceivePack(HttpServletRequest req) { - String uri = req.getRequestURI(); - return uri != null && uri.endsWith(RECEIVE_PACK_PATH) && RECEIVE_PACK_REQUEST_TYPE.equals(req.getContentType()); - } - - private GitSmartHttpTools() { - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/HttpServerText.java b/src/main/java/org/eclipse/jgit/http/server/HttpServerText.java deleted file mode 100644 index c3ce0bb6a4e6..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/HttpServerText.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2010, Sasa Zivkov and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import org.eclipse.jgit.nls.NLS; -import org.eclipse.jgit.nls.TranslationBundle; - -/** - * Translation bundle for JGit http server - */ -public class HttpServerText extends TranslationBundle { - - /** - * Get an instance of this translation bundle - * - * @return an instance of this translation bundle - */ - public static HttpServerText get() { - return NLS.getBundleFor(HttpServerText.class); - } - - // @formatter:off - /***/ public String alreadyInitializedByContainer; - /***/ public String cannotGetLengthOf; - /***/ public String encodingNotSupportedByThisLibrary; - /***/ public String expectedRepositoryAttribute; - /***/ public String filterMustNotBeNull; - /***/ public String internalErrorDuringReceivePack; - /***/ public String internalErrorDuringUploadPack; - /***/ public String internalServerError; - /***/ public String internalServerErrorRequestAttributeWasAlreadySet; - /***/ public String invalidBoolean; - /***/ public String invalidIndex; - /***/ public String invalidRegexGroup; - /***/ public String noResolverAvailable; - /***/ public String parameterNotSet; - /***/ public String pathForParamNotFound; - /***/ public String pathNotSupported; - /***/ public String receivedCorruptObject; - /***/ public String repositoryAccessForbidden; - /***/ public String repositoryNotFound; - /***/ public String servletAlreadyInitialized; - /***/ public String servletMustNotBeNull; - /***/ public String servletWasAlreadyBound; - /***/ public String unexpectedeOFOn; -} diff --git a/src/main/java/org/eclipse/jgit/http/server/InfoPacksServlet.java b/src/main/java/org/eclipse/jgit/http/server/InfoPacksServlet.java deleted file mode 100644 index 00541a8c6f5e..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/InfoPacksServlet.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import java.io.Serial; -import org.eclipse.jgit.internal.storage.file.ObjectDirectory; -import org.eclipse.jgit.internal.storage.file.Pack; -import org.eclipse.jgit.lib.ObjectDatabase; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static org.eclipse.jgit.http.server.ServletUtils.getRepository; -import static org.eclipse.jgit.http.server.ServletUtils.sendPlainText; - -/** Sends the current list of pack files, sorted most recent first. */ -class InfoPacksServlet extends HttpServlet { - - @Serial - private static final long serialVersionUID = 1L; - - @Override - public void doGet(final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { - sendPlainText(packList(req), req, rsp); - } - - private static String packList(HttpServletRequest req) { - final StringBuilder out = new StringBuilder(); - final ObjectDatabase db = getRepository(req).getObjectDatabase(); - if (db instanceof ObjectDirectory) { - for (Pack pack : ((ObjectDirectory) db).getPacks()) { - out.append("P "); - out.append(pack.getPackFile().getName()); - out.append('\n'); - } - } - out.append('\n'); - return out.toString(); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/InfoRefsServlet.java b/src/main/java/org/eclipse/jgit/http/server/InfoRefsServlet.java deleted file mode 100644 index 7730401bef18..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/InfoRefsServlet.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Serial; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.RefAdvertiser; -import org.eclipse.jgit.util.HttpSupport; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.eclipse.jgit.http.server.ServletUtils.getRepository; - -/** Send a complete list of current refs, including peeled values for tags. */ -class InfoRefsServlet extends HttpServlet { - - @Serial - private static final long serialVersionUID = 1L; - - @Override - public void doGet(final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { - // Assume a dumb client and send back the dumb client - // version of the info/refs file. - rsp.setContentType(HttpSupport.TEXT_PLAIN); - rsp.setCharacterEncoding(UTF_8.name()); - - final Repository repository = getRepository(req); - try (OutputStreamWriter out = new OutputStreamWriter(new SmartOutputStream(req, rsp, true), UTF_8)) { - final RefAdvertiser advertiser = new RefAdvertiser() { - - @Override - protected void writeOne(CharSequence line) throws IOException { - // Whoever decided that info/refs should use a different - // delimiter than the native git:// protocol shouldn't - // be allowed to design this sort of stuff. :-( - out.append(line.toString().replace(' ', '\t')); - } - - @Override - protected void end() { - // No end marker required for info/refs format. - } - }; - advertiser.init(repository); - advertiser.setDerefTags(true); - advertiser.send(repository.getRefDatabase().getRefsByPrefix(Constants.R_REFS)); - } - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/IsLocalFilter.java b/src/main/java/org/eclipse/jgit/http/server/IsLocalFilter.java deleted file mode 100644 index 21973deb52e7..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/IsLocalFilter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import org.eclipse.jgit.internal.storage.file.ObjectDirectory; -import org.eclipse.jgit.lib.Repository; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static org.eclipse.jgit.http.server.ServletUtils.getRepository; - -/** - * Requires the target {@link Repository} to be available via local filesystem. - *

    - * The target {@link Repository} must be using a {@link ObjectDirectory}, so the - * downstream servlet can directly access its contents on disk. - */ -class IsLocalFilter implements Filter { - - @Override - public void init(FilterConfig config) throws ServletException { - // Do nothing. - } - - @Override - public void destroy() { - // Do nothing. - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - if (isLocal(getRepository(request))) { - chain.doFilter(request, response); - } - else { - ((HttpServletResponse) response).sendError(SC_FORBIDDEN); - } - } - - private static boolean isLocal(Repository db) { - return db.getObjectDatabase() instanceof ObjectDirectory; - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/NoCacheFilter.java b/src/main/java/org/eclipse/jgit/http/server/NoCacheFilter.java deleted file mode 100644 index b15fa484b89b..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/NoCacheFilter.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletResponse; -import static org.eclipse.jgit.util.HttpSupport.HDR_CACHE_CONTROL; -import static org.eclipse.jgit.util.HttpSupport.HDR_EXPIRES; -import static org.eclipse.jgit.util.HttpSupport.HDR_PRAGMA; - -/** Add HTTP response headers to prevent caching by proxies/browsers. */ -class NoCacheFilter implements Filter { - - @Override - public void init(FilterConfig config) throws ServletException { - // Do nothing. - } - - @Override - public void destroy() { - // Do nothing. - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - HttpServletResponse rsp = (HttpServletResponse) response; - - rsp.setHeader(HDR_EXPIRES, "Fri, 01 Jan 1980 00:00:00 GMT"); - rsp.setHeader(HDR_PRAGMA, "no-cache"); - - final String nocache = "no-cache, max-age=0, must-revalidate"; - rsp.setHeader(HDR_CACHE_CONTROL, nocache); - - chain.doFilter(request, response); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/ObjectFileServlet.java b/src/main/java/org/eclipse/jgit/http/server/ObjectFileServlet.java deleted file mode 100644 index acb46b8d3067..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/ObjectFileServlet.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Serial; -import java.time.Instant; -import org.eclipse.jgit.internal.storage.file.ObjectDirectory; -import org.eclipse.jgit.lib.Repository; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; -import static jakarta.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; -import static org.eclipse.jgit.http.server.ServletUtils.getRepository; -import static org.eclipse.jgit.util.HttpSupport.HDR_ETAG; -import static org.eclipse.jgit.util.HttpSupport.HDR_IF_MODIFIED_SINCE; -import static org.eclipse.jgit.util.HttpSupport.HDR_IF_NONE_MATCH; -import static org.eclipse.jgit.util.HttpSupport.HDR_LAST_MODIFIED; - -/** Sends any object from {@code GIT_DIR/objects/??/0 38}, or any pack file. */ -abstract class ObjectFileServlet extends HttpServlet { - - @Serial - private static final long serialVersionUID = 1L; - - static class Loose extends ObjectFileServlet { - - @Serial - private static final long serialVersionUID = 1L; - - Loose() { - super("application/x-git-loose-object"); - } - - @Override - String etag(FileSender sender) { - Instant lastModified = sender.getLastModified(); - return Long.toHexString(lastModified.getEpochSecond()) + Long.toHexString(lastModified.getNano()); - } - } - - private abstract static class PackData extends ObjectFileServlet { - - @Serial - private static final long serialVersionUID = 1L; - - PackData(String contentType) { - super(contentType); - } - - @Override - String etag(FileSender sender) throws IOException { - return sender.getTailChecksum(); - } - } - - static class Pack extends PackData { - - @Serial - private static final long serialVersionUID = 1L; - - Pack() { - super("application/x-git-packed-objects"); - } - } - - static class PackIdx extends PackData { - - @Serial - private static final long serialVersionUID = 1L; - - PackIdx() { - super("application/x-git-packed-objects-toc"); - } - } - - private final String contentType; - - ObjectFileServlet(String contentType) { - this.contentType = contentType; - } - - abstract String etag(FileSender sender) throws IOException; - - @Override - public void doGet(final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { - serve(req, rsp, true); - } - - @Override - protected void doHead(final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { - serve(req, rsp, false); - } - - private void serve(final HttpServletRequest req, final HttpServletResponse rsp, final boolean sendBody) throws IOException { - final File obj = new File(objects(req), req.getPathInfo()); - final FileSender sender; - try { - sender = new FileSender(obj); - } - catch (FileNotFoundException e) { - rsp.sendError(SC_NOT_FOUND); - return; - } - - try { - final String etag = etag(sender); - // HTTP header Last-Modified header has a resolution of 1 sec, see - // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29 - final long lastModified = sender.getLastModified().getEpochSecond(); - - String ifNoneMatch = req.getHeader(HDR_IF_NONE_MATCH); - if (etag != null && etag.equals(ifNoneMatch)) { - rsp.sendError(SC_NOT_MODIFIED); - return; - } - - long ifModifiedSince = req.getDateHeader(HDR_IF_MODIFIED_SINCE); - if (0 < lastModified && lastModified < ifModifiedSince) { - rsp.sendError(SC_NOT_MODIFIED); - return; - } - - if (etag != null) { - rsp.setHeader(HDR_ETAG, etag); - } - if (0 < lastModified) { - rsp.setDateHeader(HDR_LAST_MODIFIED, lastModified); - } - rsp.setContentType(contentType); - sender.serve(req, rsp, sendBody); - } - finally { - sender.close(); - } - } - - private static File objects(HttpServletRequest req) { - final Repository db = getRepository(req); - return ((ObjectDirectory) db.getObjectDatabase()).getDirectory(); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/ReceivePackErrorHandler.java b/src/main/java/org/eclipse/jgit/http/server/ReceivePackErrorHandler.java deleted file mode 100644 index 24b33155842a..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/ReceivePackErrorHandler.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2019, Google LLC and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * http://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import org.eclipse.jgit.transport.ReceivePack; -import org.eclipse.jgit.transport.ServiceMayNotContinueException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -/** - * Handle git-receive-pack errors. - * - *

    - * This is an entry point for customizing an error handler for git-receive-pack. - * Right before calling {@link ReceivePack#receiveWithExceptionPropagation}, - * JGit will call this handler if specified through {@link GitFilter}. The - * implementation of this handler is responsible for calling - * {@link ReceivePackRunnable} and handling exceptions for clients. - * - *

    - * If a custom handler is not specified, JGit will use the default error - * handler. - * - * @since 5.7 - */ -public interface ReceivePackErrorHandler { - - /** - * Receive pack - * - * @param req - * The HTTP request - * @param rsp - * The HTTP response - * @param r - * A continuation that handles a git-receive-pack request. - * @throws IOException - * if an IO error occurred - */ - void receive(HttpServletRequest req, HttpServletResponse rsp, ReceivePackRunnable r) throws IOException; - - /** Process a git-receive-pack request. */ - interface ReceivePackRunnable { - - /** - * See {@link ReceivePack#receiveWithExceptionPropagation}. - * - * @throws ServiceMayNotContinueException - * if transport service cannot continue - * @throws IOException - * if an IO error occurred - */ - void receive() throws ServiceMayNotContinueException, IOException; - } - -} diff --git a/src/main/java/org/eclipse/jgit/http/server/ReceivePackServlet.java b/src/main/java/org/eclipse/jgit/http/server/ReceivePackServlet.java deleted file mode 100644 index 0337bb913d4a..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/ReceivePackServlet.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import java.io.Serial; -import java.text.MessageFormat; -import java.util.List; -import org.eclipse.jgit.annotations.Nullable; -import org.eclipse.jgit.errors.CorruptObjectException; -import org.eclipse.jgit.errors.PackProtocolException; -import org.eclipse.jgit.errors.UnpackException; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.InternalHttpServerGlue; -import org.eclipse.jgit.transport.ReceivePack; -import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; -import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; -import static jakarta.servlet.http.HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.RECEIVE_PACK; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.RECEIVE_PACK_REQUEST_TYPE; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.RECEIVE_PACK_RESULT_TYPE; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.sendError; -import static org.eclipse.jgit.http.server.ServletUtils.ATTRIBUTE_HANDLER; -import static org.eclipse.jgit.http.server.ServletUtils.consumeRequestBody; -import static org.eclipse.jgit.http.server.ServletUtils.getInputStream; -import static org.eclipse.jgit.http.server.ServletUtils.getRepository; -import static org.eclipse.jgit.util.HttpSupport.HDR_USER_AGENT; - -/** Server side implementation of smart push over HTTP. */ -class ReceivePackServlet extends HttpServlet { - - @Serial - private static final long serialVersionUID = 1L; - - static class InfoRefs extends SmartServiceInfoRefs { - - private final ReceivePackFactory receivePackFactory; - - InfoRefs(ReceivePackFactory receivePackFactory, List filters) { - super(RECEIVE_PACK, filters); - this.receivePackFactory = receivePackFactory; - } - - @Override - protected void begin(HttpServletRequest request, Repository repository) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { - ReceivePack receivePack = receivePackFactory.create(request, repository); - InternalHttpServerGlue.setPeerUserAgent(receivePack, request.getHeader(HDR_USER_AGENT)); - request.setAttribute(ATTRIBUTE_HANDLER, receivePack); - } - - @Override - protected void advertise(HttpServletRequest request, PacketLineOutRefAdvertiser advertiser) throws IOException { - ReceivePack receivePack = (ReceivePack) request.getAttribute(ATTRIBUTE_HANDLER); - try { - receivePack.sendAdvertisedRefs(advertiser); - } - finally { - receivePack.getRevWalk().close(); - } - } - } - - static class Factory implements Filter { - - private final ReceivePackFactory receivePackFactory; - - Factory(ReceivePackFactory receivePackFactory) { - this.receivePackFactory = receivePackFactory; - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - HttpServletResponse httpResponse = (HttpServletResponse) response; - ReceivePack receivePack; - try { - receivePack = receivePackFactory.create(httpServletRequest, getRepository(httpServletRequest)); - } - catch (ServiceNotAuthorizedException e) { - httpResponse.sendError(SC_UNAUTHORIZED, e.getMessage()); - return; - } - catch (ServiceNotEnabledException e) { - sendError(httpServletRequest, httpResponse, SC_FORBIDDEN, e.getMessage()); - return; - } - - try { - httpServletRequest.setAttribute(ATTRIBUTE_HANDLER, receivePack); - chain.doFilter(httpServletRequest, httpResponse); - } - finally { - httpServletRequest.removeAttribute(ATTRIBUTE_HANDLER); - } - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // Nothing. - } - - @Override - public void destroy() { - // Nothing. - } - } - - @Nullable - private final ReceivePackErrorHandler handler; - - ReceivePackServlet(@Nullable ReceivePackErrorHandler handler) { - this.handler = handler; - } - - @Override - public void doPost(final HttpServletRequest request, final HttpServletResponse response) throws IOException { - if (!RECEIVE_PACK_REQUEST_TYPE.equals(request.getContentType())) { - response.sendError(SC_UNSUPPORTED_MEDIA_TYPE); - return; - } - - SmartOutputStream out = new SmartOutputStream(request, response, false) { - - @Override - public void flush() throws IOException { - doFlush(); - } - }; - - ReceivePack receivePack = (ReceivePack) request.getAttribute(ATTRIBUTE_HANDLER); - receivePack.setBiDirectionalPipe(false); - response.setContentType(RECEIVE_PACK_RESULT_TYPE); - - if (handler != null) { - handler.receive(request, response, () -> { - receivePack.receiveWithExceptionPropagation(getInputStream(request), out, null); - out.close(); - }); - } - else { - try { - receivePack.receive(getInputStream(request), out, null); - out.close(); - } - catch (CorruptObjectException e) { - // This should be already reported to the client. - getServletContext().log(MessageFormat.format(HttpServerText.get().receivedCorruptObject, e.getMessage(), ServletUtils.identify(receivePack.getRepository()))); - consumeRequestBody(request); - out.close(); - - } - catch (UnpackException | PackProtocolException e) { - // This should be already reported to the client. - log(receivePack.getRepository(), e.getCause()); - consumeRequestBody(request); - out.close(); - - } - catch (Throwable e) { - log(receivePack.getRepository(), e); - if (!response.isCommitted()) { - response.reset(); - sendError(request, response, SC_INTERNAL_SERVER_ERROR); - } - } - } - } - - private void log(Repository repository, Throwable e) { - getServletContext().log(MessageFormat.format(HttpServerText.get().internalErrorDuringReceivePack, ServletUtils.identify(repository)), e); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/RepositoryFilter.java b/src/main/java/org/eclipse/jgit/http/server/RepositoryFilter.java deleted file mode 100644 index 298b70220fbf..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/RepositoryFilter.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import java.text.MessageFormat; -import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.ServiceMayNotContinueException; -import org.eclipse.jgit.transport.resolver.RepositoryResolver; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jgit.util.StringUtils; -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; -import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; -import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.sendError; -import static org.eclipse.jgit.http.server.ServletUtils.ATTRIBUTE_REPOSITORY; - -/** - * Open a repository named by the path info through - * {@link org.eclipse.jgit.transport.resolver.RepositoryResolver}. - *

    - * This filter assumes it is invoked by - * {@link org.eclipse.jgit.http.server.GitServlet} and is likely to not work as - * expected if called from any other class. This filter assumes the path info of - * the current request is a repository name which can be used by the configured - * {@link org.eclipse.jgit.transport.resolver.RepositoryResolver} to open a - * {@link org.eclipse.jgit.lib.Repository} and attach it to the current request. - *

    - * This filter sets request attribute - * {@link org.eclipse.jgit.http.server.ServletUtils#ATTRIBUTE_REPOSITORY} when - * it discovers the repository, and automatically closes and removes the - * attribute when the request is complete. - */ -public class RepositoryFilter implements Filter { - - private final RepositoryResolver resolver; - - private ServletContext context; - - /** - * Create a new filter. - * - * @param resolver - * the resolver which will be used to translate the URL name - * component to the actual - * {@link org.eclipse.jgit.lib.Repository} instance for the - * current web request. - */ - public RepositoryFilter(RepositoryResolver resolver) { - this.resolver = resolver; - } - - @Override - public void init(FilterConfig config) throws ServletException { - context = config.getServletContext(); - } - - @Override - public void destroy() { - context = null; - } - - @Override - public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { - HttpServletRequest req = (HttpServletRequest) request; - HttpServletResponse res = (HttpServletResponse) response; - - if (request.getAttribute(ATTRIBUTE_REPOSITORY) != null) { - context.log(MessageFormat.format(HttpServerText.get().internalServerErrorRequestAttributeWasAlreadySet, ATTRIBUTE_REPOSITORY, getClass().getName())); - sendError(req, res, SC_INTERNAL_SERVER_ERROR); - return; - } - - String name = req.getPathInfo(); - - while (!StringUtils.isEmptyOrNull(name) && name.charAt(0) == '/') { - name = name.substring(1); - } - if (StringUtils.isEmptyOrNull(name)) { - sendError(req, res, SC_NOT_FOUND); - return; - } - - try (Repository db = resolver.open(req, name)) { - request.setAttribute(ATTRIBUTE_REPOSITORY, db); - chain.doFilter(request, response); - } - catch (RepositoryNotFoundException e) { - sendError(req, res, SC_NOT_FOUND); - } - catch (ServiceNotEnabledException e) { - sendError(req, res, SC_FORBIDDEN, e.getMessage()); - } - catch (ServiceNotAuthorizedException e) { - res.sendError(SC_UNAUTHORIZED, e.getMessage()); - } - catch (ServiceMayNotContinueException e) { - sendError(req, res, e.getStatusCode(), e.getMessage()); - } - finally { - request.removeAttribute(ATTRIBUTE_REPOSITORY); - } - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/ServletUtils.java b/src/main/java/org/eclipse/jgit/http/server/ServletUtils.java deleted file mode 100644 index 2723e58bc8af..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/ServletUtils.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.MessageDigest; -import java.text.MessageFormat; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Repository; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP; -import static org.eclipse.jgit.util.HttpSupport.ENCODING_X_GZIP; -import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING; -import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING; -import static org.eclipse.jgit.util.HttpSupport.HDR_ETAG; -import static org.eclipse.jgit.util.HttpSupport.TEXT_PLAIN; - -/** - * Common utility functions for servlets. - */ -public final class ServletUtils { - - /** Request attribute which stores the {@link Repository} instance. */ - public static final String ATTRIBUTE_REPOSITORY = "org.eclipse.jgit.Repository"; - - /** Request attribute storing either UploadPack or ReceivePack. */ - public static final String ATTRIBUTE_HANDLER = "org.eclipse.jgit.transport.UploadPackOrReceivePack"; - - /** - * Get the selected repository from the request. - * - * @param req - * the current request. - * @return the repository; never null. - * @throws IllegalStateException - * the repository was not set by the filter, the servlet is - * being invoked incorrectly and the programmer should ensure - * the filter runs before the servlet. - * @see #ATTRIBUTE_REPOSITORY - */ - public static Repository getRepository(ServletRequest req) { - Repository db = (Repository) req.getAttribute(ATTRIBUTE_REPOSITORY); - if (db == null) { - throw new IllegalStateException(HttpServerText.get().expectedRepositoryAttribute); - } - return db; - } - - /** - * Open the request input stream, automatically inflating if necessary. - *

    - * This method automatically inflates the input stream if the request - * {@code Content-Encoding} header was set to {@code gzip} or the legacy - * {@code x-gzip}. - * - * @param req - * the incoming request whose input stream needs to be opened. - * @return an input stream to read the raw, uncompressed request body. - * @throws IOException - * if an input or output exception occurred. - */ - public static InputStream getInputStream(HttpServletRequest req) throws IOException { - InputStream in = req.getInputStream(); - final String enc = req.getHeader(HDR_CONTENT_ENCODING); - if (ENCODING_GZIP.equals(enc) || ENCODING_X_GZIP.equals(enc)) { - in = new GZIPInputStream(in); - } - else if (enc != null) { - throw new IOException(MessageFormat.format(HttpServerText.get().encodingNotSupportedByThisLibrary, HDR_CONTENT_ENCODING, enc)); - } - return in; - } - - /** - * Consume the entire request body, if one was supplied. - * - * @param req - * the request whose body must be consumed. - */ - public static void consumeRequestBody(HttpServletRequest req) { - if (0 < req.getContentLength() || isChunked(req)) { - try { - consumeRequestBody(req.getInputStream()); - } - catch (IOException e) { - // Ignore any errors obtaining the input stream. - } - } - } - - static boolean isChunked(HttpServletRequest req) { - return "chunked".equals(req.getHeader("Transfer-Encoding")); - } - - /** - * Consume the rest of the input stream and discard it. - * - * @param in - * the stream to discard, closed if not null. - */ - public static void consumeRequestBody(InputStream in) { - if (in == null) { - return; - } - try (in) { - while (0 < in.skip(2048) || 0 <= in.read()) { - // Discard until EOF. - } - } - catch (IOException err) { - // Discard IOException during read or skip. - } - // Discard IOException during close of input stream. - } - - /** - * Send a plain text response to a {@code GET} or {@code HEAD} HTTP request. - *

    - * The text response is encoded in the Git character encoding, UTF-8. - *

    - * If the user agent supports a compressed transfer encoding and the content - * is large enough, the content may be compressed before sending. - *

    - * The {@code ETag} and {@code Content-Length} headers are automatically set - * by this method. {@code Content-Encoding} is conditionally set if the user - * agent supports a compressed transfer. Callers are responsible for setting - * any cache control headers. - * - * @param content - * to return to the user agent as this entity's body. - * @param req - * the incoming request. - * @param rsp - * the outgoing response. - * @throws IOException - * the servlet API rejected sending the body. - */ - public static void sendPlainText(final String content, final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { - final byte[] raw = content.getBytes(UTF_8); - rsp.setContentType(TEXT_PLAIN); - rsp.setCharacterEncoding(UTF_8.name()); - send(raw, req, rsp); - } - - /** - * Send a response to a {@code GET} or {@code HEAD} HTTP request. - *

    - * If the user agent supports a compressed transfer encoding and the content - * is large enough, the content may be compressed before sending. - *

    - * The {@code ETag} and {@code Content-Length} headers are automatically set - * by this method. {@code Content-Encoding} is conditionally set if the user - * agent supports a compressed transfer. Callers are responsible for setting - * {@code Content-Type} and any cache control headers. - * - * @param content - * to return to the user agent as this entity's body. - * @param req - * the incoming request. - * @param rsp - * the outgoing response. - * @throws IOException - * the servlet API rejected sending the body. - */ - public static void send(byte[] content, final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { - content = sendInit(content, req, rsp); - try (OutputStream out = rsp.getOutputStream()) { - out.write(content); - out.flush(); - } - } - - private static byte[] sendInit(byte[] content, final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { - rsp.setHeader(HDR_ETAG, etag(content)); - if (256 < content.length && acceptsGzipEncoding(req)) { - content = compress(content); - rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP); - } - rsp.setContentLength(content.length); - return content; - } - - static boolean acceptsGzipEncoding(HttpServletRequest req) { - return acceptsGzipEncoding(req.getHeader(HDR_ACCEPT_ENCODING)); - } - - static boolean acceptsGzipEncoding(String accepts) { - if (accepts == null) { - return false; - } - - int b = 0; - while (b < accepts.length()) { - int comma = accepts.indexOf(',', b); - int e = 0 <= comma ? comma : accepts.length(); - String term = accepts.substring(b, e).trim(); - if (term.equals(ENCODING_GZIP)) { - return true; - } - b = e + 1; - } - return false; - } - - private static byte[] compress(byte[] raw) throws IOException { - final int maxLen = raw.length + 32; - final ByteArrayOutputStream out = new ByteArrayOutputStream(maxLen); - final GZIPOutputStream gz = new GZIPOutputStream(out); - gz.write(raw); - gz.finish(); - gz.flush(); - return out.toByteArray(); - } - - private static String etag(byte[] content) { - final MessageDigest md = Constants.newMessageDigest(); - md.update(content); - return ObjectId.fromRaw(md.digest()).getName(); - } - - static String identify(Repository repository) { - String identifier = repository.getIdentifier(); - if (identifier == null) { - return "unknown"; - } - return identifier; - } - - private ServletUtils() { - // static utility class only - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/SmartOutputStream.java b/src/main/java/org/eclipse/jgit/http/server/SmartOutputStream.java deleted file mode 100644 index 4eb1309a7121..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/SmartOutputStream.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.zip.GZIPOutputStream; -import org.eclipse.jgit.util.TemporaryBuffer; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static org.eclipse.jgit.http.server.ServletUtils.acceptsGzipEncoding; -import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP; -import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING; - -/** - * Buffers a response, trying to gzip it if the user agent supports that. - *

    - * If the response overflows the buffer, gzip is skipped and the response is - * streamed to the client as its produced, most likely using HTTP/1.1 chunked - * encoding. This is useful for servlets that produce mixed-mode content, where - * smaller payloads are primarily pure text that compresses well, while much - * larger payloads are heavily compressed binary data. {@link UploadPackServlet} - * is one such servlet. - */ -class SmartOutputStream extends TemporaryBuffer { - - private static final int LIMIT = 32 * 1024; - - private final HttpServletRequest request; - - private final HttpServletResponse response; - - private final boolean compressStream; - - private boolean startedOutput; - - SmartOutputStream(final HttpServletRequest request, final HttpServletResponse response, boolean compressStream) { - super(LIMIT); - this.request = request; - this.response = response; - this.compressStream = compressStream; - } - - @Override - protected OutputStream overflow() throws IOException { - startedOutput = true; - - OutputStream out = response.getOutputStream(); - if (compressStream && acceptsGzipEncoding(request)) { - response.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP); - out = new GZIPOutputStream(out); - } - return out; - } - - @Override - public void close() throws IOException { - super.close(); - - if (!startedOutput) { - // If output hasn't started yet, the entire thing fit into our - // buffer. Try to use a proper Content-Length header, and also - // deflate the response with gzip if it will be smaller. - if (256 < this.length() && acceptsGzipEncoding(request)) { - TemporaryBuffer gzbuf = new TemporaryBuffer.Heap(LIMIT); - try { - try (GZIPOutputStream gzip = new GZIPOutputStream(gzbuf)) { - this.writeTo(gzip, null); - } - if (gzbuf.length() < this.length()) { - response.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP); - writeResponse(gzbuf); - return; - } - } - catch (IOException err) { - // Most likely caused by overflowing the buffer, meaning - // its larger if it were compressed. Discard compressed - // copy and use the original. - } - } - writeResponse(this); - } - } - - private void writeResponse(TemporaryBuffer out) throws IOException { - // The Content-Length cannot overflow when cast to an int, our - // hardcoded LIMIT constant above assures us we wouldn't store - // more than 2 GiB of content in memory. - response.setContentLength((int) out.length()); - try (OutputStream os = response.getOutputStream()) { - out.writeTo(os, null); - os.flush(); - } - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java b/src/main/java/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java deleted file mode 100644 index ea7547155256..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import java.util.List; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.PacketLineOut; -import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser; -import org.eclipse.jgit.transport.ServiceMayNotContinueException; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.infoRefsResultType; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.sendError; -import static org.eclipse.jgit.http.server.ServletUtils.ATTRIBUTE_HANDLER; -import static org.eclipse.jgit.http.server.ServletUtils.getRepository; - -/** Filter in front of {@link InfoRefsServlet} to catch smart service requests. */ -abstract class SmartServiceInfoRefs implements Filter { - - private final String service; - - private final Filter[] filters; - - SmartServiceInfoRefs(String service, List filters) { - this.service = service; - this.filters = filters.toArray(new Filter[0]); - } - - @Override - public void init(FilterConfig config) throws ServletException { - // Do nothing. - } - - @Override - public void destroy() { - // Do nothing. - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - final HttpServletRequest req = (HttpServletRequest) request; - final HttpServletResponse res = (HttpServletResponse) response; - - if (service.equals(req.getParameter("service"))) { - final Repository db = getRepository(req); - try { - begin(req, db); - } - catch (ServiceNotAuthorizedException e) { - res.sendError(SC_UNAUTHORIZED, e.getMessage()); - return; - } - catch (ServiceNotEnabledException e) { - sendError(req, res, SC_FORBIDDEN, e.getMessage()); - return; - } - - try { - if (filters.length == 0) { - service(req, response); - } - else { - new Chain().doFilter(request, response); - } - } - finally { - req.removeAttribute(ATTRIBUTE_HANDLER); - } - } - else { - chain.doFilter(request, response); - } - } - - private void service(ServletRequest request, ServletResponse response) throws IOException { - final HttpServletRequest req = (HttpServletRequest) request; - final HttpServletResponse res = (HttpServletResponse) response; - final SmartOutputStream buf = new SmartOutputStream(req, res, true); - try { - res.setContentType(infoRefsResultType(service)); - - final PacketLineOut out = new PacketLineOut(buf); - respond(req, out, service); - buf.close(); - } - catch (ServiceNotAuthorizedException e) { - res.sendError(SC_UNAUTHORIZED, e.getMessage()); - } - catch (ServiceNotEnabledException e) { - sendError(req, res, SC_FORBIDDEN, e.getMessage()); - } - catch (ServiceMayNotContinueException e) { - if (e.isOutput()) { - buf.close(); - } - else { - sendError(req, res, e.getStatusCode(), e.getMessage()); - } - } - } - - /** - * Begin service. - * - * @param req - * request - * @param db - * repository - * @throws IOException - * if an IO error occurred - * @throws ServiceNotEnabledException - * if a service is not available - * @throws ServiceNotAuthorizedException - * if service requires authentication and the current user - * didn't provide credentials - */ - protected abstract void begin(HttpServletRequest req, Repository db) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException; - - /** - * Advertise. - * - * @param req - * request - * @param pck - * used to frame lines in PacketLineOut format - * @throws IOException - * if an IO error occurred - * @throws ServiceNotEnabledException - * if a service is not available - * @throws ServiceNotAuthorizedException - * if service requires authentication and the current user - * didn't provide credentials - */ - protected abstract void advertise(HttpServletRequest req, PacketLineOutRefAdvertiser pck) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException; - - /** - * Writes the appropriate response to an info/refs request received by a - * smart service. In protocol v0, this starts with "# service=serviceName" - * followed by a flush packet, but this is not necessarily the case in other - * protocol versions. - *

    - * The default implementation writes "# service=serviceName" and a flush - * packet, then calls {@link #advertise}. Subclasses should override this - * method if they support protocol versions other than protocol v0. - * - * @param req - * request - * @param pckOut - * destination of response - * @param serviceName - * service name to be written out in protocol v0; may or may not - * be used in other versions - * @throws IOException - * if an IO error occurred - * @throws ServiceNotEnabledException - * if a service is not available - * @throws ServiceNotAuthorizedException - * if service requires authentication and the current user - * didn't provide credentials - */ - protected void respond(HttpServletRequest req, PacketLineOut pckOut, String serviceName) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { - pckOut.writeString("# service=" + service + '\n'); //$NON-NLS-1$ - pckOut.end(); - advertise(req, new PacketLineOutRefAdvertiser(pckOut)); - } - - private class Chain implements FilterChain { - - private int filterIdx; - - @Override - public void doFilter(ServletRequest req, ServletResponse rsp) throws IOException, ServletException { - if (filterIdx < filters.length) { - filters[filterIdx++].doFilter(req, rsp, this); - } - else { - service(req, rsp); - } - } - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/TextFileServlet.java b/src/main/java/org/eclipse/jgit/http/server/TextFileServlet.java deleted file mode 100644 index 003534b453ef..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/TextFileServlet.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Serial; -import org.eclipse.jgit.util.HttpSupport; -import org.eclipse.jgit.util.IO; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; -import static org.eclipse.jgit.http.server.ServletUtils.getRepository; -import static org.eclipse.jgit.http.server.ServletUtils.send; - -/** Sends a small text meta file from the repository. */ -class TextFileServlet extends HttpServlet { - - @Serial - private static final long serialVersionUID = 1L; - - private final String fileName; - - TextFileServlet(String name) { - this.fileName = name; - } - - @Override - public void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException { - try { - response.setContentType(HttpSupport.TEXT_PLAIN); - send(read(request), request, response); - } - catch (FileNotFoundException noFile) { - response.sendError(SC_NOT_FOUND); - } - } - - private byte[] read(HttpServletRequest request) throws IOException { - final File gitdir = getRepository(request).getDirectory(); - if (gitdir == null) { - throw new FileNotFoundException(fileName); - } - return IO.readFully(new File(gitdir, fileName)); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/UploadPackErrorHandler.java b/src/main/java/org/eclipse/jgit/http/server/UploadPackErrorHandler.java deleted file mode 100644 index 757ae4db4f2e..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/UploadPackErrorHandler.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2019, Google LLC and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * http://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import org.eclipse.jgit.errors.PackProtocolException; -import org.eclipse.jgit.transport.ServiceMayNotContinueException; -import org.eclipse.jgit.transport.UploadPack; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; -import static jakarta.servlet.http.HttpServletResponse.SC_OK; - -/** - * Handle git-upload-pack errors. - * - *

    - * This is an entry point for customizing an error handler for git-upload-pack. - * Right before calling {@link UploadPack#uploadWithExceptionPropagation}, JGit - * will call this handler if specified through {@link GitFilter}. The - * implementation of this handler is responsible for calling - * {@link UploadPackRunnable} and handling exceptions for clients. - * - *

    - * If a custom handler is not specified, JGit will use the default error - * handler. - * - * @since 5.6 - */ -public interface UploadPackErrorHandler { - - /** - * Maps a thrown git related Exception to an appropriate HTTP status code. - * - * @param error - * The thrown Exception. - * @return the HTTP status code as an int - * @since 6.1.1 - */ - static int statusCodeForThrowable(Throwable error) { - if (error instanceof ServiceNotEnabledException) { - return SC_FORBIDDEN; - } - if (error instanceof PackProtocolException) { - // Internal git errors are not errors from an HTTP standpoint. - return SC_OK; - } - return SC_INTERNAL_SERVER_ERROR; - } - - /** - * Upload pack - * - * @param request - * The HTTP request - * @param response - * The HTTP response - * @param runnable - * A continuation that handles a git-upload-pack request. - * @throws IOException - * if an IO error occurred - */ - void upload(HttpServletRequest request, HttpServletResponse response, UploadPackRunnable runnable) throws IOException; - - /** Process a git-upload-pack request. */ - interface UploadPackRunnable { - - /** - * See {@link UploadPack#uploadWithExceptionPropagation}. - * - * @throws ServiceMayNotContinueException - * transport service cannot continue - * @throws IOException - * if an IO error occurred - */ - void upload() throws ServiceMayNotContinueException, IOException; - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/UploadPackServlet.java b/src/main/java/org/eclipse/jgit/http/server/UploadPackServlet.java deleted file mode 100644 index f3f7bb8291ec..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/UploadPackServlet.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server; - -import java.io.IOException; -import java.io.Serial; -import java.text.MessageFormat; -import java.util.List; -import org.eclipse.jgit.annotations.Nullable; -import org.eclipse.jgit.errors.PackProtocolException; -import org.eclipse.jgit.http.server.UploadPackErrorHandler.UploadPackRunnable; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.InternalHttpServerGlue; -import org.eclipse.jgit.transport.PacketLineOut; -import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser; -import org.eclipse.jgit.transport.ServiceMayNotContinueException; -import org.eclipse.jgit.transport.UploadPack; -import org.eclipse.jgit.transport.UploadPackInternalServerErrorException; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; -import static jakarta.servlet.http.HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.UPLOAD_PACK; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.UPLOAD_PACK_REQUEST_TYPE; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.UPLOAD_PACK_RESULT_TYPE; -import static org.eclipse.jgit.http.server.GitSmartHttpTools.sendError; -import static org.eclipse.jgit.http.server.ServletUtils.ATTRIBUTE_HANDLER; -import static org.eclipse.jgit.http.server.ServletUtils.consumeRequestBody; -import static org.eclipse.jgit.http.server.ServletUtils.getInputStream; -import static org.eclipse.jgit.http.server.ServletUtils.getRepository; -import static org.eclipse.jgit.http.server.UploadPackErrorHandler.statusCodeForThrowable; -import static org.eclipse.jgit.util.HttpSupport.HDR_USER_AGENT; - -/** Server side implementation of smart fetch over HTTP. */ -class UploadPackServlet extends HttpServlet { - - @Serial - private static final long serialVersionUID = 1L; - - static class InfoRefs extends SmartServiceInfoRefs { - - private final UploadPackFactory uploadPackFactory; - - InfoRefs(UploadPackFactory uploadPackFactory, List filters) { - super(UPLOAD_PACK, filters); - this.uploadPackFactory = uploadPackFactory; - } - - @Override - protected void begin(HttpServletRequest request, Repository repository) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { - UploadPack up = uploadPackFactory.create(request, repository); - InternalHttpServerGlue.setPeerUserAgent(up, request.getHeader(HDR_USER_AGENT)); - request.setAttribute(ATTRIBUTE_HANDLER, up); - } - - @Override - protected void advertise(HttpServletRequest request, PacketLineOutRefAdvertiser advertiser) throws IOException { - UploadPack up = (UploadPack) request.getAttribute(ATTRIBUTE_HANDLER); - try { - up.setBiDirectionalPipe(false); - up.sendAdvertisedRefs(advertiser); - } - finally { - up.getRevWalk().close(); - } - } - - @Override - protected void respond(HttpServletRequest request, PacketLineOut pckOut, String serviceName) - throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { - UploadPack uploadPack = (UploadPack) request.getAttribute(ATTRIBUTE_HANDLER); - try { - uploadPack.setBiDirectionalPipe(false); - uploadPack.sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut), serviceName); - } - finally { - uploadPack.getRevWalk().close(); - } - } - } - - static class Factory implements Filter { - - private final UploadPackFactory uploadPackFactory; - - Factory(UploadPackFactory uploadPackFactory) { - this.uploadPackFactory = uploadPackFactory; - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpRequest = (HttpServletRequest) request; - HttpServletResponse httpResponse = (HttpServletResponse) response; - UploadPack uploadPack; - try { - uploadPack = uploadPackFactory.create(httpRequest, getRepository(httpRequest)); - } - catch (ServiceNotAuthorizedException e) { - httpResponse.sendError(SC_UNAUTHORIZED, e.getMessage()); - return; - } - catch (ServiceNotEnabledException e) { - sendError(httpRequest, httpResponse, SC_FORBIDDEN, e.getMessage()); - return; - } - - try { - httpRequest.setAttribute(ATTRIBUTE_HANDLER, uploadPack); - chain.doFilter(httpRequest, httpResponse); - } - finally { - httpRequest.removeAttribute(ATTRIBUTE_HANDLER); - } - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // Nothing. - } - - @Override - public void destroy() { - // Nothing. - } - } - - private final UploadPackErrorHandler handler; - - UploadPackServlet(@Nullable UploadPackErrorHandler handler) { - this.handler = handler != null ? handler : this::defaultUploadPackHandler; - } - - @Override - public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { - if (!UPLOAD_PACK_REQUEST_TYPE.equals(request.getContentType())) { - response.sendError(SC_UNSUPPORTED_MEDIA_TYPE); - return; - } - - handler.upload(request, response, () -> upload(request, response)); - } - - private void upload(HttpServletRequest request, HttpServletResponse response) throws IOException { - // to be explicitly closed by caller - SmartOutputStream out = new SmartOutputStream(request, response, false) { - - @Override - public void flush() throws IOException { - doFlush(); - } - }; - Repository repo = null; - try (UploadPack up = (UploadPack) request.getAttribute(ATTRIBUTE_HANDLER)) { - up.setBiDirectionalPipe(false); - response.setContentType(UPLOAD_PACK_RESULT_TYPE); - repo = up.getRepository(); - up.uploadWithExceptionPropagation(getInputStream(request), out, null); - out.close(); - } - catch (ServiceMayNotContinueException e) { - if (e.isOutput()) { - consumeRequestBody(request); - out.close(); - } - throw e; - } - catch (UploadPackInternalServerErrorException e) { - // Special case exception, error message was sent to client. - log(repo, e.getCause()); - consumeRequestBody(request); - out.close(); - } - } - - private void defaultUploadPackHandler(HttpServletRequest request, HttpServletResponse response, UploadPackRunnable runnable) throws IOException { - try { - runnable.upload(); - } - catch (ServiceMayNotContinueException e) { - if (!e.isOutput() && !response.isCommitted()) { - response.reset(); - sendError(request, response, e.getStatusCode(), e.getMessage()); - } - } - catch (Throwable e) { - UploadPack up = (UploadPack) request.getAttribute(ATTRIBUTE_HANDLER); - log(up.getRepository(), e); - if (!response.isCommitted()) { - response.reset(); - String msg = null; - if (e instanceof PackProtocolException || e instanceof ServiceNotEnabledException) { - msg = e.getMessage(); - } - sendError(request, response, statusCodeForThrowable(e), msg); - } - } - } - - private void log(Repository git, Throwable e) { - getServletContext().log(MessageFormat.format(HttpServerText.get().internalErrorDuringUploadPack, ServletUtils.identify(git)), e); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/ErrorServlet.java b/src/main/java/org/eclipse/jgit/http/server/glue/ErrorServlet.java deleted file mode 100644 index 086445ef9642..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/ErrorServlet.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import java.io.IOException; -import java.io.Serial; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -/** - * Send a fixed status code to the client. - */ -public class ErrorServlet extends HttpServlet { - - @Serial - private static final long serialVersionUID = 1L; - - private final int status; - - /** - * Sends a specific status code. - * - * @param status - * the HTTP status code to always send. - */ - public ErrorServlet(int status) { - this.status = status; - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { - rsp.sendError(status); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/MetaFilter.java b/src/main/java/org/eclipse/jgit/http/server/glue/MetaFilter.java deleted file mode 100644 index ad113c2aa70d..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/MetaFilter.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import java.io.IOException; -import java.text.MessageFormat; -import java.util.AbstractSet; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import org.eclipse.jgit.http.server.HttpServerText; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -/** - * Generic container filter to manage routing to different pipelines. - *

    - * Callers can create and configure a new processing pipeline by using one of - * the {@link #serve(String)} or {@link #serveRegex(String)} methods to allocate - * a binder for a particular URL pattern. - *

    - * Registered filters and servlets are initialized lazily, usually during the - * first request. Once initialized the bindings in this servlet cannot be - * modified without destroying the servlet and thereby destroying all registered - * filters and servlets. - */ -public class MetaFilter implements Filter { - - static final String REGEX_GROUPS = "org.eclipse.jgit.http.server.glue.MetaServlet.serveRegex"; - - private ServletContext servletContext; - - private final List bindings; - - private volatile UrlPipeline[] pipelines; - - /** - * Empty filter with no bindings. - */ - public MetaFilter() { - this.bindings = new ArrayList<>(); - } - - /** - * Construct a binding for a specific path. - * - * @param path - * pattern to match. - * @return binder for the passed path. - */ - public ServletBinder serve(String path) { - if (path.startsWith("*")) { - return register(new SuffixPipeline.Binder(path.substring(1))); - } - throw new IllegalArgumentException(MessageFormat.format(HttpServerText.get().pathNotSupported, path)); - } - - /** - * Construct a binding for a regular expression. - * - * @param expression - * the regular expression to pattern match the URL against. - * @return binder for the passed expression. - */ - public ServletBinder serveRegex(String expression) { - return register(new RegexPipeline.Binder(expression)); - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - servletContext = filterConfig.getServletContext(); - } - - @Override - public void destroy() { - if (pipelines != null) { - Set destroyed = newIdentitySet(); - for (UrlPipeline p : pipelines) { - p.destroy(destroyed); - } - pipelines = null; - } - } - - private static Set newIdentitySet() { - final IdentityHashMap m = new IdentityHashMap<>(); - return new AbstractSet<>() { - - @Override - public boolean add(Object o) { - return m.put(o, o) == null; - } - - @Override - public boolean contains(Object o) { - return m.containsKey(o); - } - - @Override - public Iterator iterator() { - return m.keySet().iterator(); - } - - @Override - public int size() { - return m.size(); - } - }; - } - - @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - HttpServletRequest req = (HttpServletRequest) request; - HttpServletResponse res = (HttpServletResponse) response; - UrlPipeline p = find(req); - if (p != null) { - p.service(req, res); - } - else { - chain.doFilter(req, res); - } - } - - private UrlPipeline find(HttpServletRequest req) throws ServletException { - for (UrlPipeline p : getPipelines()) { - if (p.match(req)) { - return p; - } - } - return null; - } - - private ServletBinder register(ServletBinderImpl b) { - synchronized (bindings) { - if (pipelines != null) { - throw new IllegalStateException(HttpServerText.get().servletAlreadyInitialized); - } - bindings.add(b); - } - return register((ServletBinder) b); - } - - /** - * Configure a newly created binder. - * - * @param b - * the newly created binder. - * @return binder for the caller, potentially after adding one or more - * filters into the pipeline. - */ - protected ServletBinder register(ServletBinder b) { - return b; - } - - private UrlPipeline[] getPipelines() throws ServletException { - UrlPipeline[] r = pipelines; - if (r == null) { - synchronized (bindings) { - r = pipelines; - if (r == null) { - r = createPipelines(); - pipelines = r; - } - } - } - return r; - } - - private UrlPipeline[] createPipelines() throws ServletException { - UrlPipeline[] array = new UrlPipeline[bindings.size()]; - - for (int i = 0; i < bindings.size(); i++) { - array[i] = bindings.get(i).create(); - } - - Set inited = newIdentitySet(); - for (UrlPipeline p : array) { - p.init(servletContext, inited); - } - return array; - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/MetaServlet.java b/src/main/java/org/eclipse/jgit/http/server/glue/MetaServlet.java deleted file mode 100644 index 629e83ede9b9..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/MetaServlet.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import java.io.IOException; -import java.io.Serial; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; - -/** - * Generic container servlet to manage routing to different pipelines. - *

    - * Callers can create and configure a new processing pipeline by using one of - * the {@link #serve(String)} or {@link #serveRegex(String)} methods to allocate - * a binder for a particular URL pattern. - *

    - * Registered filters and servlets are initialized lazily, usually during the - * first request. Once initialized the bindings in this servlet cannot be - * modified without destroying the servlet and thereby destroying all registered - * filters and servlets. - */ -public class MetaServlet extends HttpServlet { - - @Serial - private static final long serialVersionUID = 1L; - - private final MetaFilter filter; - - /** - * Initialize a servlet wrapping a filter. - * - * @param delegateFilter - * the filter being wrapped by the servlet. - */ - protected MetaServlet(MetaFilter delegateFilter) { - filter = delegateFilter; - } - - /** - * Get delegate filter - * - * @return filter this servlet delegates all routing logic to. - */ - protected MetaFilter getDelegateFilter() { - return filter; - } - - /** - * Construct a binding for a specific path. - * - * @param path - * pattern to match. - * @return binder for the passed path. - */ - public ServletBinder serve(String path) { - return filter.serve(path); - } - - /** - * Construct a binding for a regular expression. - * - * @param expression - * the regular expression to pattern match the URL against. - * @return binder for the passed expression. - */ - public ServletBinder serveRegex(String expression) { - return filter.serveRegex(expression); - } - - @Override public void init(ServletConfig config) throws ServletException { - String name = filter.getClass().getName(); - ServletContext ctx = config.getServletContext(); - filter.init(new NoParameterFilterConfig(name, ctx)); - } - - @Override public void destroy() { - filter.destroy(); - } - - @Override protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { - filter.doFilter(req, res, (ServletRequest request, ServletResponse response) -> { - ((HttpServletResponse) response).sendError(SC_NOT_FOUND); - }); - } - - /** - * Configure a newly created binder. - * - * @param b - * the newly created binder. - * @return binder for the caller, potentially after adding one or more - * filters into the pipeline. - */ - protected ServletBinder register(ServletBinder b) { - return filter.register(b); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/NoParameterFilterConfig.java b/src/main/java/org/eclipse/jgit/http/server/glue/NoParameterFilterConfig.java deleted file mode 100644 index c46730e163c1..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/NoParameterFilterConfig.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import java.util.Enumeration; -import java.util.NoSuchElementException; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletContext; - -final class NoParameterFilterConfig implements FilterConfig { - - private final String filterName; - - private final ServletContext context; - - NoParameterFilterConfig(String filterName, ServletContext context) { - this.filterName = filterName; - this.context = context; - } - - @Override - public String getInitParameter(String name) { - return null; - } - - @Override - public Enumeration getInitParameterNames() { - return new Enumeration<>() { - - @Override - public boolean hasMoreElements() { - return false; - } - - @Override - public String nextElement() { - throw new NoSuchElementException(); - } - }; - } - - @Override - public ServletContext getServletContext() { - return context; - } - - @Override - public String getFilterName() { - return filterName; - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/RegexGroupFilter.java b/src/main/java/org/eclipse/jgit/http/server/glue/RegexGroupFilter.java deleted file mode 100644 index e3fcbaf558e3..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/RegexGroupFilter.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import java.io.IOException; -import java.text.MessageFormat; -import org.eclipse.jgit.http.server.HttpServerText; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; - -/** - * Switch servlet path and path info to use another regex match group. - *

    - * This filter is meant to be installed in the middle of a pipeline created by - * {@link org.eclipse.jgit.http.server.glue.MetaServlet#serveRegex(String)}. The - * passed request's servlet path is updated to be all text up to the start of - * the designated capture group, and the path info is changed to the contents of - * the capture group. - */ -public class RegexGroupFilter implements Filter { - - private final int groupIdx; - - /** - * Constructor for RegexGroupFilter - * - * @param groupIdx - * capture group number, 1 through the number of groups. - */ - public RegexGroupFilter(int groupIdx) { - if (groupIdx < 1) { - throw new IllegalArgumentException(MessageFormat.format(HttpServerText.get().invalidIndex, groupIdx)); - } - this.groupIdx = groupIdx - 1; - } - - @Override - public void init(FilterConfig config) throws ServletException { - // Do nothing. - } - - @Override - public void destroy() { - // Do nothing. - } - - @Override - public void doFilter(final ServletRequest request, final ServletResponse rsp, final FilterChain chain) throws IOException, ServletException { - final WrappedRequest[] g = groupsFor(request); - if (groupIdx < g.length) { - chain.doFilter(g[groupIdx], rsp); - } - else { - throw new ServletException(MessageFormat.format(HttpServerText.get().invalidRegexGroup, groupIdx + 1)); - } - } - - private static WrappedRequest[] groupsFor(ServletRequest r) { - return (WrappedRequest[]) r.getAttribute(MetaFilter.REGEX_GROUPS); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/RegexPipeline.java b/src/main/java/org/eclipse/jgit/http/server/glue/RegexPipeline.java deleted file mode 100644 index 40068b0f6bb4..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/RegexPipeline.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import jakarta.servlet.Filter; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; -import static org.eclipse.jgit.http.server.glue.MetaFilter.REGEX_GROUPS; - -/** - * Selects requests by matching the URI against a regular expression. - *

    - * The pattern is bound and matched against the path info of the servlet - * request, as this class assumes it is invoked by {@link MetaServlet}. - *

    - * If there are capture groups in the regular expression, the matched ranges of - * the capture groups are stored as an array of modified HttpServetRequests, - * into the request attribute {@link MetaFilter#REGEX_GROUPS}. Using a capture - * group that may not capture, e.g. {@code "(/foo)?"}, will cause an error at - * request handling time. - *

    - * Each servlet request has been altered to have its {@code getServletPath()} - * method return the original path info up to the beginning of the corresponding - * capture group, and its {@code getPathInfo()} method return the matched text. - * A {@link RegexGroupFilter} can be applied in the pipeline to switch the - * current HttpServletRequest to reference a different capture group before - * running additional filters, or the final servlet. - *

    - * Note that for {@code getPathInfo()} to start with a leading "/" as described - * in the servlet documentation, capture groups must actually capture the - * leading "/". - *

    - * This class dispatches the remainder of the pipeline using the first capture - * group as the current request, making {@code RegexGroupFilter} required only - * to access capture groups beyond the first. - */ -class RegexPipeline extends UrlPipeline { - - static class Binder extends ServletBinderImpl { - - private final Pattern pattern; - - Binder(String p) { - pattern = Pattern.compile(p); - } - - @Override - UrlPipeline create() { - return new RegexPipeline(pattern, getFilters(), getServlet()); - } - } - - private final Pattern pattern; - - RegexPipeline(final Pattern pattern, final Filter[] filters, final HttpServlet servlet) { - super(filters, servlet); - this.pattern = pattern; - } - - @Override - boolean match(HttpServletRequest req) { - final String pathInfo = req.getPathInfo(); - return pathInfo != null && pattern.matcher(pathInfo).matches(); - } - - @Override - void service(HttpServletRequest req, HttpServletResponse rsp) throws ServletException, IOException { - final String reqInfo = req.getPathInfo(); - if (reqInfo == null) { - rsp.sendError(SC_NOT_FOUND); - return; - } - - final Matcher cur = pattern.matcher(reqInfo); - if (!cur.matches()) { - rsp.sendError(SC_NOT_FOUND); - return; - } - - final String reqPath = req.getServletPath(); - final Object old = req.getAttribute(REGEX_GROUPS); - try { - if (1 <= cur.groupCount()) { - // If there are groups extract every capture group and - // build a request for them so RegexGroupFilter can pick - // a different capture group later. Continue using the - // first capture group as the path info. - WrappedRequest[] groups = new WrappedRequest[cur.groupCount()]; - for (int groupId = 1; groupId <= cur.groupCount(); groupId++) { - final int s = cur.start(groupId); - final String path, info; - - path = reqPath + reqInfo.substring(0, s); - info = cur.group(groupId); - groups[groupId - 1] = new WrappedRequest(req, path, info); - } - req.setAttribute(REGEX_GROUPS, groups); - super.service(groups[0], rsp); - - } - else { - // No capture groups were present, service the whole request. - final String path = reqPath + reqInfo; - final String info = null; - super.service(new WrappedRequest(req, path, info), rsp); - } - } - finally { - if (old != null) { - req.setAttribute(REGEX_GROUPS, old); - } - else { - req.removeAttribute(REGEX_GROUPS); - } - } - } - - @Override - public String toString() { - return "Pipeline[regex: " + pattern + " ]"; - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/ServletBinder.java b/src/main/java/org/eclipse/jgit/http/server/glue/ServletBinder.java deleted file mode 100644 index 1d01a34cd520..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/ServletBinder.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import jakarta.servlet.Filter; -import jakarta.servlet.http.HttpServlet; - -/** - * Binds a servlet to a URL. - */ -public interface ServletBinder { - - /** - * Set the filter to trigger while processing the path. - * - * @param filter - * the filter to trigger while processing the path. - * @return {@code this}. - */ - ServletBinder through(Filter filter); - - /** - * Set the servlet to execute on this path - * - * @param servlet - * the servlet to execute on this path. - */ - void with(HttpServlet servlet); -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/ServletBinderImpl.java b/src/main/java/org/eclipse/jgit/http/server/glue/ServletBinderImpl.java deleted file mode 100644 index 634b85b42378..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/ServletBinderImpl.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import java.util.ArrayList; -import java.util.List; -import org.eclipse.jgit.http.server.HttpServerText; -import jakarta.servlet.Filter; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletResponse; - -abstract class ServletBinderImpl implements ServletBinder { - - private final List filters; - - private HttpServlet httpServlet; - - ServletBinderImpl() { - this.filters = new ArrayList<>(); - } - - @Override - public ServletBinder through(Filter filter) { - if (filter == null) { - throw new NullPointerException(HttpServerText.get().filterMustNotBeNull); - } - filters.add(filter); - return this; - } - - @Override - public void with(HttpServlet servlet) { - if (servlet == null) { - throw new NullPointerException(HttpServerText.get().servletMustNotBeNull); - } - if (httpServlet != null) { - throw new IllegalStateException(HttpServerText.get().servletWasAlreadyBound); - } - httpServlet = servlet; - } - - /** - * Get the servlet - * - * @return the configured servlet, or singleton returning 404 if none. - */ - protected HttpServlet getServlet() { - if (httpServlet != null) { - return httpServlet; - } - return new ErrorServlet(HttpServletResponse.SC_NOT_FOUND); - } - - /** - * Get filters - * - * @return the configured filters; zero-length array if none. - */ - protected Filter[] getFilters() { - return filters.toArray(new Filter[0]); - } - - /** - * Create UrlPipeline - * - * @return the pipeline that matches and executes this chain. - */ - abstract UrlPipeline create(); -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/SuffixPipeline.java b/src/main/java/org/eclipse/jgit/http/server/glue/SuffixPipeline.java deleted file mode 100644 index 0ce6d019ac35..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/SuffixPipeline.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import java.io.IOException; -import jakarta.servlet.Filter; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -/** - * Selects requests by matching the suffix of the URI. - *

    - * The suffix string is literally matched against the path info of the servlet - * request, as this class assumes it is invoked by {@link MetaServlet}. Suffix - * strings may include path components. Examples include {@code /info/refs}, or - * just simple extension matches like {@code .txt}. - *

    - * When dispatching to the rest of the pipeline the HttpServletRequest is - * modified so that {@code getPathInfo()} does not contain the suffix that - * caused this pipeline to be selected. - */ -class SuffixPipeline extends UrlPipeline { - - static class Binder extends ServletBinderImpl { - - private final String suffix; - - Binder(String suffix) { - this.suffix = suffix; - } - - @Override - UrlPipeline create() { - return new SuffixPipeline(suffix, getFilters(), getServlet()); - } - } - - private final String suffix; - - private final int suffixLen; - - SuffixPipeline(final String suffix, final Filter[] filters, final HttpServlet servlet) { - super(filters, servlet); - this.suffix = suffix; - this.suffixLen = suffix.length(); - } - - @Override - boolean match(HttpServletRequest req) { - final String pathInfo = req.getPathInfo(); - return pathInfo != null && pathInfo.endsWith(suffix); - } - - @Override - void service(HttpServletRequest req, HttpServletResponse rsp) throws ServletException, IOException { - String curInfo = req.getPathInfo(); - String newPath = req.getServletPath() + curInfo; - String newInfo = curInfo.substring(0, curInfo.length() - suffixLen); - super.service(new WrappedRequest(req, newPath, newInfo), rsp); - } - - @Override - public String toString() { - return "Pipeline[ *" + suffix + " ]"; - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/UrlPipeline.java b/src/main/java/org/eclipse/jgit/http/server/glue/UrlPipeline.java deleted file mode 100644 index 70ab17772a01..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/UrlPipeline.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import java.io.IOException; -import java.util.Enumeration; -import java.util.NoSuchElementException; -import java.util.Set; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -/** - * Encapsulates the entire serving stack for a single URL. - *

    - * Subclasses provide the implementation of {@link #match(HttpServletRequest)}, - * which is called by {@link MetaServlet} in registration order to determine the - * pipeline that will be used to handle a request. - *

    - * The very bottom of each pipeline is a single {@link HttpServlet} that will - * handle producing the response for this pipeline's URL. {@link Filter}s may - * also be registered and applied around the servlet's processing, to manage - * request attributes, set standard response headers, or completely override the - * response generation. - */ -abstract class UrlPipeline { - - /** Filters to apply around {@link #servlet}; may be empty but never null. */ - private final Filter[] filters; - - /** Instance that must generate the response; never null. */ - private final HttpServlet servlet; - - UrlPipeline(Filter[] filters, HttpServlet servlet) { - this.filters = filters; - this.servlet = servlet; - } - - /** - * Initialize all contained filters and servlets. - * - * @param context - * the servlet container context our {@link MetaServlet} is - * running within. - * @param inited - * (input/output) the set of filters and servlets which - * have already been initialized within the container context. If - * those same instances appear in this pipeline they are not - * initialized a second time. Filters and servlets that are first - * initialized by this pipeline will be added to this set. - * @throws ServletException - * a filter or servlet is unable to initialize. - */ - void init(ServletContext context, Set inited) throws ServletException { - for (Filter ref : filters) { - initFilter(ref, context, inited); - } - initServlet(servlet, context, inited); - } - - private static void initFilter(final Filter ref, final ServletContext context, final Set inited) throws ServletException { - if (!inited.contains(ref)) { - ref.init(new NoParameterFilterConfig(ref.getClass().getName(), context)); - inited.add(ref); - } - } - - private static void initServlet(final HttpServlet ref, final ServletContext context, final Set inited) throws ServletException { - if (!inited.contains(ref)) { - ref.init(new ServletConfig() { - - @Override - public String getInitParameter(String name) { - return null; - } - - @Override - public Enumeration getInitParameterNames() { - return new Enumeration<>() { - - @Override - public boolean hasMoreElements() { - return false; - } - - @Override - public String nextElement() { - throw new NoSuchElementException(); - } - }; - } - - @Override - public ServletContext getServletContext() { - return context; - } - - @Override - public String getServletName() { - return ref.getClass().getName(); - } - }); - inited.add(ref); - } - } - - /** - * Destroy all contained filters and servlets. - * - * @param destroyed - * (input/output) the set of filters and servlets which - * have already been destroyed within the container context. If - * those same instances appear in this pipeline they are not - * destroyed a second time. Filters and servlets that are first - * destroyed by this pipeline will be added to this set. - */ - void destroy(Set destroyed) { - for (Filter ref : filters) { - destroyFilter(ref, destroyed); - } - destroyServlet(servlet, destroyed); - } - - private static void destroyFilter(Filter ref, Set destroyed) { - if (!destroyed.contains(ref)) { - ref.destroy(); - destroyed.add(ref); - } - } - - private static void destroyServlet(HttpServlet ref, Set destroyed) { - if (!destroyed.contains(ref)) { - ref.destroy(); - destroyed.add(ref); - } - } - - /** - * Determine if this pipeline handles the request's URL. - *

    - * This method should match on the request's {@code getPathInfo()} method, - * as {@link MetaServlet} passes the request along as-is to each pipeline's - * match method. - * - * @param req - * current HTTP request being considered by {@link MetaServlet}. - * @return {@code true} if this pipeline is configured to handle the - * request; {@code false} otherwise. - */ - abstract boolean match(HttpServletRequest req); - - /** - * Execute the filters and the servlet on the request. - *

    - * Invoked by {@link MetaServlet} once {@link #match(HttpServletRequest)} - * has determined this pipeline is the correct pipeline to handle the - * current request. - * - * @param req - * current HTTP request. - * @param rsp - * current HTTP response. - * @throws ServletException - * request cannot be completed. - * @throws IOException - * IO error prevents the request from being completed. - */ - void service(HttpServletRequest req, HttpServletResponse rsp) throws ServletException, IOException { - if (0 < filters.length) { - new Chain(filters, servlet).doFilter(req, rsp); - } - else { - servlet.service(req, rsp); - } - } - - private static class Chain implements FilterChain { - - private final Filter[] filters; - - private final HttpServlet servlet; - - private int filterIdx; - - Chain(Filter[] filters, HttpServlet servlet) { - this.filters = filters; - this.servlet = servlet; - } - - @Override - public void doFilter(ServletRequest req, ServletResponse rsp) throws IOException, ServletException { - if (filterIdx < filters.length) { - filters[filterIdx++].doFilter(req, rsp, this); - } - else { - servlet.service(req, rsp); - } - } - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/glue/WrappedRequest.java b/src/main/java/org/eclipse/jgit/http/server/glue/WrappedRequest.java deleted file mode 100644 index ed24f4f795cb..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/glue/WrappedRequest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.glue; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletRequestWrapper; - -/** - * Overrides the path and path info. - */ -public class WrappedRequest extends HttpServletRequestWrapper { - - private final String path; - - private final String pathInfo; - - /** - * Create a new request with different path and path info properties. - * - * @param originalRequest - * the original HTTP request. - * @param path - * new servlet path to report to callers. - * @param pathInfo - * new path info to report to callers. - */ - public WrappedRequest(final HttpServletRequest originalRequest, final String path, final String pathInfo) { - super(originalRequest); - this.path = path; - this.pathInfo = pathInfo; - } - - @Override - public String getPathTranslated() { - final String p = getPathInfo(); - return p != null ? getSession().getServletContext().getRealPath(p) : null; - } - - @Override - public String getPathInfo() { - return pathInfo; - } - - @Override - public String getServletPath() { - return path; - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/resolver/AsIsFileService.java b/src/main/java/org/eclipse/jgit/http/server/resolver/AsIsFileService.java deleted file mode 100644 index 48f0f8316cad..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/resolver/AsIsFileService.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.resolver; - -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import jakarta.servlet.http.HttpServletRequest; - -/** - * Controls access to bare files in a repository. - *

    - * Older HTTP clients which do not speak the smart HTTP variant of the Git - * protocol fetch from a repository by directly getting its objects and pack - * files. This class, along with the {@code http.getanyfile} per-repository - * configuration setting, can be used by - * {@link org.eclipse.jgit.http.server.GitServlet} to control whether or not - * these older clients are permitted to read these direct files. - */ -public class AsIsFileService { - - /** Always throws {@link ServiceNotEnabledException}. */ - public static final AsIsFileService DISABLED = new AsIsFileService() { - - @Override public void access(HttpServletRequest req, Repository db) throws ServiceNotEnabledException { - throw new ServiceNotEnabledException(); - } - }; - - private static class ServiceConfig { - - final boolean enabled; - - ServiceConfig(Config cfg) { - enabled = cfg.getBoolean("http", "getanyfile", true); - } - } - - /** - * Determine if {@code http.getanyfile} is enabled in the configuration. - * - * @param db - * the repository to check. - * @return {@code false} if {@code http.getanyfile} was explicitly set to - * {@code false} in the repository's configuration file; otherwise - * {@code true}. - */ - protected static boolean isEnabled(Repository db) { - return db.getConfig().get(ServiceConfig::new).enabled; - } - - /** - * Determine if access to any bare file of the repository is allowed. - *

    - * This method silently succeeds if the request is allowed, or fails by - * throwing a checked exception if access should be denied. - *

    - * The default implementation of this method checks {@code http.getanyfile}, - * throwing - * {@link org.eclipse.jgit.transport.resolver.ServiceNotEnabledException} if - * it was explicitly set to {@code false}, and otherwise succeeding - * silently. - * - * @param req - * current HTTP request, in case information from the request may - * help determine the access request. - * @param db - * the repository the request would obtain a bare file from. - * @throws ServiceNotEnabledException - * bare file access is not allowed on the target repository, by - * any user, for any reason. - * @throws ServiceNotAuthorizedException - * bare file access is not allowed for this HTTP request and - * repository, such as due to a permission error. - */ - public void access(HttpServletRequest req, Repository db) throws ServiceNotEnabledException, ServiceNotAuthorizedException { - if (!isEnabled(db)) { - throw new ServiceNotEnabledException(); - } - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/resolver/DefaultReceivePackFactory.java b/src/main/java/org/eclipse/jgit/http/server/resolver/DefaultReceivePackFactory.java deleted file mode 100644 index b169b34fd91f..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/resolver/DefaultReceivePackFactory.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.resolver; - -import org.eclipse.jgit.util.StringUtils; -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.ReceivePack; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import jakarta.servlet.http.HttpServletRequest; - -/** - * Create and configure {@link org.eclipse.jgit.transport.ReceivePack} service - * instance. - *

    - * Writing by receive-pack is permitted if any of the following is true: - *

      - *
    • The container has authenticated the user and set - * {@link jakarta.servlet.http.HttpServletRequest#getRemoteUser()} to the - * authenticated name. - *
    • The repository configuration file has {@code http.receivepack} explicitly - * set to true. - *
    - * and explicitly rejected otherwise. - */ -public class DefaultReceivePackFactory implements ReceivePackFactory { - - private static class ServiceConfig { - - final boolean set; - - final boolean enabled; - - ServiceConfig(Config cfg) { - set = cfg.getString("http", null, "receivepack") != null; - enabled = cfg.getBoolean("http", "receivepack", false); - } - } - - @Override - public ReceivePack create(HttpServletRequest req, Repository db) throws ServiceNotEnabledException, ServiceNotAuthorizedException { - final ServiceConfig cfg = db.getConfig().get(ServiceConfig::new); - String user = req.getRemoteUser(); - - if (cfg.set) { - if (cfg.enabled) { - if (StringUtils.isEmptyOrNull(user)) { - user = "anonymous"; - } - return createFor(req, db, user); - } - throw new ServiceNotEnabledException(); - } - - if (!StringUtils.isEmptyOrNull(user)) { - return createFor(req, db, user); - } - throw new ServiceNotAuthorizedException(); - } - - private static ReceivePack createFor(final HttpServletRequest req, final Repository db, final String user) { - final ReceivePack rp = new ReceivePack(db); - rp.setRefLogIdent(toPersonIdent(req, user)); - return rp; - } - - private static PersonIdent toPersonIdent(HttpServletRequest req, String user) { - return new PersonIdent(user, user + "@" + req.getRemoteHost()); - } -} diff --git a/src/main/java/org/eclipse/jgit/http/server/resolver/DefaultUploadPackFactory.java b/src/main/java/org/eclipse/jgit/http/server/resolver/DefaultUploadPackFactory.java deleted file mode 100644 index 1d9e991d650b..000000000000 --- a/src/main/java/org/eclipse/jgit/http/server/resolver/DefaultUploadPackFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2009-2010, Google Inc. and others - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * SPDX-License-Identifier: BSD-3-Clause - */ - -package org.eclipse.jgit.http.server.resolver; - -import java.util.Arrays; -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.UploadPack; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; -import jakarta.servlet.http.HttpServletRequest; - -/** - * Create and configure {@link org.eclipse.jgit.transport.UploadPack} service - * instance. - *

    - * Reading by upload-pack is permitted unless {@code http.uploadpack} is - * explicitly set to false. - */ -public class DefaultUploadPackFactory implements UploadPackFactory { - - private static class ServiceConfig { - - final boolean enabled; - - ServiceConfig(Config cfg) { - enabled = cfg.getBoolean("http", "uploadpack", true); - } - } - - @Override - public UploadPack create(HttpServletRequest req, Repository db) throws ServiceNotEnabledException, ServiceNotAuthorizedException { - if (db.getConfig().get(ServiceConfig::new).enabled) { - UploadPack up = new UploadPack(db); - String header = req.getHeader("Git-Protocol"); //$NON-NLS-1$ - if (header != null) { - String[] params = header.split(":"); //$NON-NLS-1$ - up.setExtraParameters(Arrays.asList(params)); - } - return up; - } - throw new ServiceNotEnabledException(); - } -} diff --git a/src/main/kubernetes/artemis/statefulsets/artemis-mysql.yml b/src/main/kubernetes/artemis/statefulsets/artemis-mysql.yml index a60037ddb2f0..2b8dc16c7ec8 100644 --- a/src/main/kubernetes/artemis/statefulsets/artemis-mysql.yml +++ b/src/main/kubernetes/artemis/statefulsets/artemis-mysql.yml @@ -31,7 +31,7 @@ spec: envFrom: - configMapRef: name: artemis-mysql - image: mysql:8.4.0 + image: mysql:9.0.1 imagePullPolicy: IfNotPresent name: artemis-mysql ports: diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index f699af193497..6de99b3986d9 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -177,7 +177,7 @@ zonky: type: H2 # Alternatives: H2 / MYSQL / POSTGRES postgres: docker: - image: "postgres:16.3-alpine" + image: "postgres:16.4-alpine" tmpfs: enabled: true server: @@ -200,7 +200,7 @@ zonky: max_parallel_maintenance_workers: 4 mysql: docker: - image: "mysql:8.4.0" + image: "mysql:9.0.1" tmpfs: enabled: true From b7233066a33ca2e6b1214fef8ccd151408dd744b Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Tue, 10 Sep 2024 12:37:05 +0200 Subject: [PATCH 09/67] Development: Fix an issue with the used JUnit5 version --- build.gradle | 5 ++--- docs/dev/setup.rst | 4 ++-- gradle.properties | 13 +++++++++---- package-lock.json | 2 +- package.json | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 8b20a431cc56..a3402dcf7e44 100644 --- a/build.gradle +++ b/build.gradle @@ -533,11 +533,10 @@ dependencies { // Lightweight JSON library needed for the internals of the MockRestServiceServer testImplementation "org.json:json:20240303" - testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.11.0" + // NOTE: make sure this corresponds to the version used for JUnit in the testImplementation + testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}" } -ext["junit-jupiter.version"] = junit_version - dependencyManagement { imports { mavenBom "io.zonky.test.postgres:embedded-postgres-binaries-bom:16.4.0" diff --git a/docs/dev/setup.rst b/docs/dev/setup.rst index 5fc206dbc925..3ed91396dc39 100644 --- a/docs/dev/setup.rst +++ b/docs/dev/setup.rst @@ -32,10 +32,10 @@ following dependencies/tools on your machine: 2. `MySQL Database Server 8 `__, or `PostgreSQL `_: Artemis uses Hibernate to store entities in an SQL database and Liquibase to automatically apply schema transformations when updating Artemis. -3. `Node.js `__: We use Node LTS (>=20.14.0 < 21) to compile +3. `Node.js `__: We use Node LTS (>=20.16.0 < 21) to compile and run the client Angular application. Depending on your system, you can install Node either from source or as a pre-packaged bundle. -4. `Npm `__: We use Npm (>=10.7.0) to +4. `Npm `__: We use Npm (>=10.8.0) to manage client side dependencies. Npm is typically bundled with Node.js, but can also be installed separately. 5. ( `Graphviz `__: We use Graphviz to generate graphs within exercise task diff --git a/gradle.properties b/gradle.properties index fb4f6273eefe..5cac2d2e4f08 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,8 +2,8 @@ rootProject.name=Artemis profile=dev # Build properties -node_version=20.14.0 -npm_version=10.7.0 +node_version=20.16.0 +npm_version=10.8.0 # Dependency versions jhipster_dependencies_version=8.7.0 @@ -16,8 +16,6 @@ opensaml_version=4.3.2 jwt_version=0.12.6 jaxb_runtime_version=4.0.5 hazelcast_version=5.5.0 -junit_version=5.10.2 -mockito_version=5.13.0 fasterxml_version=2.17.2 jgit_version=7.0.0.202409031743-r sshd_version=2.13.2 @@ -31,6 +29,13 @@ logback_version=1.5.8 java_parser_version=3.26.2 byte_buddy_version=1.15.1 +# testing +# make sure both versions are compatible +junit_version=5.11.0 +junit_platform_version=1.11.0 +mockito_version=5.13.0 + + # gradle plugin version gradle_node_plugin_version=7.0.2 apt_plugin_version=0.21 diff --git a/package-lock.json b/package-lock.json index 0131e10e4aa4..157f2024adc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,7 +126,7 @@ "weak-napi": "2.0.2" }, "engines": { - "node": ">=20.14.0" + "node": ">=20.16.0" } }, "node_modules/@ampproject/remapping": { diff --git a/package.json b/package.json index 94177e7ee86e..a3ce41ccc798 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,7 @@ "weak-napi": "2.0.2" }, "engines": { - "node": ">=20.14.0" + "node": ">=20.16.0" }, "scripts": { "build": "npm run webapp:prod --", From 2cf010d0783357a5b52737e6b812d52c2fb001e4 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Tue, 10 Sep 2024 13:09:58 +0200 Subject: [PATCH 10/67] Development: Bump version to 7.5.3 --- README.md | 2 +- build.gradle | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bbfc8945e870..4bd470ce51ce 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ Refer to [Using JHipster in production](http://www.jhipster.tech/production) for The following command can automate the deployment to a server. The example shows the deployment to the main Artemis test server (which runs a virtual machine): ```shell -./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.5.2.war +./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.5.3.war ``` ## Architecture diff --git a/build.gradle b/build.gradle index a3402dcf7e44..be02c9dc2a2f 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ plugins { } group = "de.tum.in.www1.artemis" -version = "7.5.2" +version = "7.5.3" description = "Interactive Learning with Individual Feedback" java { diff --git a/package-lock.json b/package-lock.json index 157f2024adc7..153c556fc286 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "artemis", - "version": "7.5.2", + "version": "7.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis", - "version": "7.5.2", + "version": "7.5.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index a3ce41ccc798..d4f2b273c939 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "artemis", - "version": "7.5.2", + "version": "7.5.3", "description": "Interactive Learning with Individual Feedback", "private": true, "license": "MIT", From 1264ae2a3165158a09fab4ffc21a92ed2ae1599c Mon Sep 17 00:00:00 2001 From: Enea Gore <73840596+EneaGore@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:09:43 +0200 Subject: [PATCH 11/67] Text exercises: Add preliminary AI feedback requests for students on text exercises using Athena (#9241) --- .../tum/in/www1/artemis/domain/Exercise.java | 5 + .../in/www1/artemis/domain/Submission.java | 10 ++ .../artemis/service/ParticipationService.java | 31 ++++ .../service/TextExerciseFeedbackService.java | 149 ++++++++++++++++++ .../service/TextSubmissionService.java | 8 +- .../web/rest/AbstractSubmissionResource.java | 4 + .../web/rest/ParticipationResource.java | 68 ++++++-- .../web/rest/TextExerciseResource.java | 10 +- .../web/websocket/ResultWebsocketService.java | 4 +- .../webapp/app/entities/submission.model.ts | 7 +- .../exercise-scores.component.ts | 12 +- .../manage-assessment-buttons.component.html | 4 +- ...feedback-suggestion-options.component.html | 14 ++ ...e-feedback-suggestion-options.component.ts | 12 ++ .../shared/feedback/feedback.component.html | 2 +- .../shared/result/result.component.html | 1 + .../shared/result/result.component.ts | 1 + .../exercises/shared/result/result.service.ts | 21 ++- .../exercises/shared/result/result.utils.ts | 39 ++++- .../text/assess/text-assessment.service.ts | 2 +- .../text-submission-assessment.component.ts | 2 +- .../text-exercise-detail.component.ts | 1 + .../participate/text-editor.component.html | 4 +- .../text/participate/text-editor.component.ts | 8 +- .../course-exercise-details.component.html | 2 + .../course-exercise-details.component.ts | 23 ++- ...ise-details-student-actions.component.html | 13 ++ ...rcise-details-student-actions.component.ts | 60 +++++-- .../result-history.component.ts | 14 +- src/main/webapp/i18n/de/exercise.json | 3 + src/main/webapp/i18n/en/exercise.json | 3 + ...actSpringIntegrationJenkinsGitlabTest.java | 4 + .../ParticipationIntegrationTest.java | 69 ++++++++ .../component/exercises/shared/result.spec.ts | 31 ++++ ...-details-student-actions.component.spec.ts | 48 +++++- .../spec/component/utils/result.utils.spec.ts | 36 +++++ 36 files changed, 647 insertions(+), 78 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/service/TextExerciseFeedbackService.java diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java index 4c103dde0732..ef1d75fde4de 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java @@ -49,6 +49,7 @@ import com.fasterxml.jackson.annotation.JsonView; import de.tum.in.www1.artemis.domain.competency.CourseCompetency; +import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.ExerciseType; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; @@ -603,6 +604,10 @@ else if (resultDate1.isAfter(resultDate2)) { public Set findResultsFilteredForStudents(Participation participation) { boolean isAssessmentOver = getAssessmentDueDate() == null || getAssessmentDueDate().isBefore(ZonedDateTime.now()); if (!isAssessmentOver) { + // This allows the showing of preliminary feedback in case the assessment due date is set before its over. + if (this instanceof TextExercise) { + return participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).collect(Collectors.toSet()); + } return Set.of(); } return participation.getResults().stream().filter(result -> result.getCompletionDate() != null).collect(Collectors.toSet()); diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java index 30f4275cc14f..b6d819ca32e3 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java @@ -213,6 +213,16 @@ public List getManualResults() { return results.stream().filter(result -> result != null && !result.isAutomatic() && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new)); } + /** + * This method is necessary to ignore Athena results in the assessment view + * + * @return non athena automatic results including null results + */ + @JsonIgnore + public List getNonAthenaResults() { + return results.stream().filter(result -> result == null || !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new)); + } + /** * Get the manual result by id of the submission * diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index d579f5765083..23f8b350ca6b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -31,6 +31,7 @@ import de.tum.in.www1.artemis.domain.enumeration.InitializationState; import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; import de.tum.in.www1.artemis.domain.participation.Participant; +import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; @@ -664,6 +665,21 @@ public Optional findOneByExerciseAndStudentLoginWithEagerS return studentParticipationRepository.findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); } + /** + * Get one participation (in any state) by its student and exercise with eager submissions else throw exception. + * + * @param exercise the exercise for which to find a participation + * @param username the username of the student + * @return the participation of the given student and exercise with eager submissions in any state + */ + public StudentParticipation findOneByExerciseAndStudentLoginWithEagerSubmissionsAnyStateElseThrow(Exercise exercise, String username) { + Optional optionalParticipation = findOneByExerciseAndStudentLoginWithEagerSubmissionsAnyState(exercise, username); + if (optionalParticipation.isEmpty()) { + throw new EntityNotFoundException("No participation found in exercise with id " + exercise.getId() + " for user " + username); + } + return optionalParticipation.get(); + } + /** * Get all exercise participations belonging to exercise and student. * @@ -693,6 +709,21 @@ public List findByExerciseAndStudentIdWithEagerSubmissions return studentParticipationRepository.findByExerciseIdAndStudentIdWithEagerLegalSubmissions(exercise.getId(), studentId); } + /** + * Get the text exercise participation with the Latest Submissions and its results + * + * @param participationId the id of the participation + * @return the participation with latest submission and result + * @throws EntityNotFoundException + */ + public StudentParticipation findTextExerciseParticipationWithLatestSubmissionAndResultElseThrow(Long participationId) throws EntityNotFoundException { + Optional participation = participationRepository.findByIdWithLatestSubmissionAndResult(participationId); + if (participation.isEmpty() || !(participation.get() instanceof StudentParticipation studentParticipation)) { + throw new EntityNotFoundException("No text exercise participation found with id " + participationId); + } + return studentParticipation; + } + /** * Get all programming exercise participations belonging to exercise and student with eager results and submissions. * diff --git a/src/main/java/de/tum/in/www1/artemis/service/TextExerciseFeedbackService.java b/src/main/java/de/tum/in/www1/artemis/service/TextExerciseFeedbackService.java new file mode 100644 index 000000000000..844f3cc20016 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/TextExerciseFeedbackService.java @@ -0,0 +1,149 @@ +package de.tum.in.www1.artemis.service; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.Feedback; +import de.tum.in.www1.artemis.domain.Result; +import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.domain.TextSubmission; +import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; +import de.tum.in.www1.artemis.domain.enumeration.FeedbackType; +import de.tum.in.www1.artemis.domain.participation.Participation; +import de.tum.in.www1.artemis.domain.participation.StudentParticipation; +import de.tum.in.www1.artemis.repository.ResultRepository; +import de.tum.in.www1.artemis.service.connectors.athena.AthenaFeedbackSuggestionsService; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; +import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; +import de.tum.in.www1.artemis.web.websocket.ResultWebsocketService; + +@Profile(PROFILE_CORE) +@Service +public class TextExerciseFeedbackService { + + private static final Logger log = LoggerFactory.getLogger(TextExerciseFeedbackService.class); + + public static final String NON_GRADED_FEEDBACK_SUGGESTION = "NonGradedFeedbackSuggestion:"; + + private final Optional athenaFeedbackSuggestionsService; + + private final ResultWebsocketService resultWebsocketService; + + private final SubmissionService submissionService; + + private final ParticipationService participationService; + + private final ResultService resultService; + + private final ResultRepository resultRepository; + + public TextExerciseFeedbackService(Optional athenaFeedbackSuggestionsService, SubmissionService submissionService, + ResultService resultService, ResultRepository resultRepository, ResultWebsocketService resultWebsocketService, ParticipationService participationService) { + this.athenaFeedbackSuggestionsService = athenaFeedbackSuggestionsService; + this.submissionService = submissionService; + this.resultService = resultService; + this.resultRepository = resultRepository; + this.resultWebsocketService = resultWebsocketService; + this.participationService = participationService; + } + + private void checkRateLimitOrThrow(StudentParticipation participation) { + + List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); + + long countOfAthenaResults = athenaResults.size(); + + if (countOfAthenaResults >= 10) { + throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met"); + } + } + + /** + * Handles the request for generating feedback for a text exercise. + * Unlike programming exercises a tutor is not notified if Athena is not available. + * + * @param exerciseId the id of the text exercise. + * @param participation the student participation associated with the exercise. + * @param textExercise the text exercise object. + * @return StudentParticipation updated text exercise for an AI assessment + */ + public StudentParticipation handleNonGradedFeedbackRequest(Long exerciseId, StudentParticipation participation, TextExercise textExercise) { + if (this.athenaFeedbackSuggestionsService.isPresent()) { + this.checkRateLimitOrThrow(participation); + CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, textExercise)); + } + return participation; + } + + /** + * Generates automatic non-graded feedback for a text exercise submission. + * This method leverages the Athena service to generate feedback based on the latest submission. + * + * @param participation the student participation associated with the exercise. + * @param textExercise the text exercise object. + */ + public void generateAutomaticNonGradedFeedback(StudentParticipation participation, TextExercise textExercise) { + log.debug("Using athena to generate (text exercise) feedback request: {}", textExercise.getId()); + + // athena takes over the control here + var submissionOptional = participationService.findTextExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId()).findLatestSubmission(); + + if (submissionOptional.isEmpty()) { + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); + } + var submission = submissionOptional.get(); + + Result automaticResult = new Result(); + automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); + automaticResult.setRated(true); + automaticResult.setScore(0.0); + automaticResult.setSuccessful(null); + automaticResult.setSubmission(submission); + automaticResult.setParticipation(participation); + try { + this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult); + + log.debug("Submission id: {}", submission.getId()); + + var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, (TextSubmission) submission, false); + + List feedbacks = athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).map(individualFeedbackItem -> { + var feedback = new Feedback(); + feedback.setText(individualFeedbackItem.title()); + feedback.setDetailText(individualFeedbackItem.description()); + feedback.setHasLongFeedbackText(false); + feedback.setType(FeedbackType.AUTOMATIC); + feedback.setCredits(individualFeedbackItem.credits()); + return feedback; + }).toList(); + + double totalFeedbacksScore = 0.0; + for (Feedback feedback : feedbacks) { + totalFeedbacksScore += feedback.getCredits(); + } + totalFeedbacksScore = totalFeedbacksScore / textExercise.getMaxPoints() * 100; + automaticResult.setSuccessful(true); + automaticResult.setCompletionDate(ZonedDateTime.now()); + + automaticResult.setScore(Math.clamp(totalFeedbacksScore, 0, 100)); + + automaticResult = this.resultRepository.save(automaticResult); + resultService.storeFeedbackInResult(automaticResult, feedbacks, true); + submissionService.saveNewResult(submission, automaticResult); + this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult); + } + catch (Exception e) { + log.error("Could not generate feedback", e); + throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated"); + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/TextSubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/TextSubmissionService.java index 11110487e211..65e28f3db003 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/TextSubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/TextSubmissionService.java @@ -81,6 +81,13 @@ public TextSubmission handleTextSubmission(TextSubmission textSubmission, TextEx if (exercise.isExamExercise() || exerciseDateService.isBeforeDueDate(participation)) { textSubmission.setSubmitted(true); } + + // if athena results are present than create new submission on submit + if (!textSubmission.getResults().isEmpty()) { + log.debug("Creating a new submission due to Athena results for user: {}", user.getLogin()); + textSubmission.setId(null); + } + textSubmission = save(textSubmission, participation, exercise, user); return textSubmission; } @@ -104,7 +111,6 @@ private TextSubmission save(TextSubmission textSubmission, StudentParticipation participation.setInitializationState(InitializationState.FINISHED); studentParticipationRepository.save(participation); } - // remove result from submission (in the unlikely case it is passed here), so that students cannot inject a result textSubmission.setResults(new ArrayList<>()); textSubmission = textSubmissionRepository.save(textSubmission); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AbstractSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AbstractSubmissionResource.java index cfd5595c9794..21d2324ee4bc 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AbstractSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AbstractSubmissionResource.java @@ -81,6 +81,10 @@ protected ResponseEntity> getAllSubmissions(Long exerciseId, bo if (submission.getParticipation() != null && submission.getParticipation().getExercise() != null) { submission.getParticipation().setExercise(null); } + // Important for exercises with Athena results + if (assessedByTutor) { + submission.setResults(submission.getNonAthenaResults()); + } }); return ResponseEntity.ok().body(submissions); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java index c12a2a4b56b8..8e5654e315d7 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java @@ -20,6 +20,7 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; +import org.apache.velocity.exception.ResourceNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -50,6 +51,7 @@ import de.tum.in.www1.artemis.domain.Submission; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.ExerciseType; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; @@ -83,6 +85,7 @@ import de.tum.in.www1.artemis.service.GradingScaleService; import de.tum.in.www1.artemis.service.ParticipationAuthorizationCheckService; import de.tum.in.www1.artemis.service.ParticipationService; +import de.tum.in.www1.artemis.service.TextExerciseFeedbackService; import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; @@ -162,6 +165,8 @@ public class ParticipationResource { private final ProgrammingExerciseCodeReviewFeedbackService programmingExerciseCodeReviewFeedbackService; + private final TextExerciseFeedbackService textExerciseFeedbackService; + public ParticipationResource(ParticipationService participationService, ProgrammingExerciseParticipationService programmingExerciseParticipationService, CourseRepository courseRepository, QuizExerciseRepository quizExerciseRepository, ExerciseRepository exerciseRepository, ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService, @@ -171,7 +176,7 @@ public ParticipationResource(ParticipationService participationService, Programm ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, SubmissionRepository submissionRepository, ResultRepository resultRepository, ExerciseDateService exerciseDateService, InstanceMessageSendService instanceMessageSendService, QuizBatchService quizBatchService, SubmittedAnswerRepository submittedAnswerRepository, QuizSubmissionService quizSubmissionService, GradingScaleService gradingScaleService, - ProgrammingExerciseCodeReviewFeedbackService programmingExerciseCodeReviewFeedbackService) { + ProgrammingExerciseCodeReviewFeedbackService programmingExerciseCodeReviewFeedbackService, TextExerciseFeedbackService textExerciseFeedbackService) { this.participationService = participationService; this.programmingExerciseParticipationService = programmingExerciseParticipationService; this.quizExerciseRepository = quizExerciseRepository; @@ -197,6 +202,7 @@ public ParticipationResource(ParticipationService participationService, Programm this.quizSubmissionService = quizSubmissionService; this.gradingScaleService = gradingScaleService; this.programmingExerciseCodeReviewFeedbackService = programmingExerciseCodeReviewFeedbackService; + this.textExerciseFeedbackService = textExerciseFeedbackService; } /** @@ -352,39 +358,70 @@ public ResponseEntity resumeParticipati @PutMapping("exercises/{exerciseId}/request-feedback") @EnforceAtLeastStudent @FeatureToggle(Feature.ProgrammingExercises) - public ResponseEntity requestFeedback(@PathVariable Long exerciseId, Principal principal) { + public ResponseEntity requestFeedback(@PathVariable Long exerciseId, Principal principal) { log.debug("REST request for feedback request: {}", exerciseId); - var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); - if (programmingExercise.isExamExercise()) { - throw new BadRequestAlertException("Not intended for the use in exams", "participation", "preconditions not met"); + Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); + + if (!(exercise instanceof TextExercise) && !(exercise instanceof ProgrammingExercise)) { + throw new BadRequestAlertException("Unsupported exercise type", "participation", "unsupported type"); } - if (programmingExercise.getDueDate() != null && now().isAfter(programmingExercise.getDueDate())) { + return handleExerciseFeedbackRequest(exercise, principal); + } + + private ResponseEntity handleExerciseFeedbackRequest(Exercise exercise, Principal principal) { + // Validate exercise and timing + if (exercise.isExamExercise()) { + throw new BadRequestAlertException("Not intended for the use in exams", "participation", "preconditions not met"); + } + if (exercise.getDueDate() != null && now().isAfter(exercise.getDueDate())) { throw new BadRequestAlertException("The due date is over", "participation", "preconditions not met"); } + if (exercise instanceof ProgrammingExercise) { + ((ProgrammingExercise) exercise).validateSettingsForFeedbackRequest(); + } - var participation = programmingExerciseParticipationService.findStudentParticipationByExerciseAndStudentId(programmingExercise, principal.getName()); + // Get and validate participation User user = userRepository.getUserWithGroupsAndAuthorities(); + StudentParticipation participation = (exercise instanceof ProgrammingExercise) + ? programmingExerciseParticipationService.findStudentParticipationByExerciseAndStudentId(exercise, principal.getName()) + : studentParticipationRepository.findByExerciseIdAndStudentLogin(exercise.getId(), principal.getName()) + .orElseThrow(() -> new ResourceNotFoundException("Participation not found")); checkAccessPermissionOwner(participation, user); - programmingExercise.validateSettingsForFeedbackRequest(); + participation = studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId()); - var studentParticipation = (ProgrammingExerciseStudentParticipation) studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId()); - var result = studentParticipation.findLatestLegalResult(); - if (result == null) { - throw new BadRequestAlertException("User has not reached the conditions to submit a feedback request", "participation", "preconditions not met"); + // Check submission requirements + if (exercise instanceof TextExercise) { + if (submissionRepository.findAllByParticipationId(participation.getId()).isEmpty()) { + throw new BadRequestAlertException("You need to submit at least once", "participation", "preconditions not met"); + } + } + else if (exercise instanceof ProgrammingExercise) { + if (participation.findLatestLegalResult() == null) { + throw new BadRequestAlertException("User has not reached the conditions to submit a feedback request", "participation", "preconditions not met"); + } } + // Check if feedback has already been requested var currentDate = now(); var participationIndividualDueDate = participation.getIndividualDueDate(); if (participationIndividualDueDate != null && currentDate.isAfter(participationIndividualDueDate)) { throw new BadRequestAlertException("Request has already been sent", "participation", "already sent"); } - participation = this.programmingExerciseCodeReviewFeedbackService.handleNonGradedFeedbackRequest(exerciseId, studentParticipation, programmingExercise); + // Process feedback request + StudentParticipation updatedParticipation; + if (exercise instanceof TextExercise) { + updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(exercise.getId(), participation, (TextExercise) exercise); + } + else { + updatedParticipation = programmingExerciseCodeReviewFeedbackService.handleNonGradedFeedbackRequest(exercise.getId(), + (ProgrammingExerciseStudentParticipation) participation, (ProgrammingExercise) exercise); + } - return ResponseEntity.ok().body(participation); + return ResponseEntity.ok().body(updatedParticipation); } /** @@ -580,7 +617,8 @@ public ResponseEntity> getAllParticipationsForExercise participations = findParticipationWithLatestResults(exercise); participations.forEach(participation -> { participation.setSubmissionCount(participation.getSubmissions().size()); - if (participation.getResults() != null && !participation.getResults().isEmpty()) { + if (participation.getResults() != null && !participation.getResults().isEmpty() + && !(participation.getResults().stream().allMatch(result -> AssessmentType.AUTOMATIC_ATHENA.equals(result.getAssessmentType())))) { participation.setSubmissions(null); } else if (participation.getSubmissions() != null && !participation.getSubmissions().isEmpty()) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java index 853e39e8c861..cb528fc8e38d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java @@ -2,12 +2,10 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; import static de.tum.in.www1.artemis.web.rest.plagiarism.PlagiarismResultResponseBuilder.buildPlagiarismResultResponse; -import static java.util.Collections.emptySet; import java.io.File; import java.net.URI; import java.net.URISyntaxException; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -41,6 +39,7 @@ import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.TextSubmission; import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; @@ -424,8 +423,11 @@ public ResponseEntity getDataForTextEditor(@PathVariable L textSubmission.setParticipation(null); if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) { - textSubmission.setResults(Collections.emptyList()); - participation.setResults(emptySet()); + // We want to have the preliminary feedback before the assessment due date too + List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); + textSubmission.setResults(athenaResults); + Set athenaResultsSet = new HashSet(athenaResults); + participation.setResults(athenaResultsSet); } Result result = textSubmission.getLatestResult(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/websocket/ResultWebsocketService.java b/src/main/java/de/tum/in/www1/artemis/web/websocket/ResultWebsocketService.java index 05e5a645b23b..ab27fb9e76b9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/websocket/ResultWebsocketService.java +++ b/src/main/java/de/tum/in/www1/artemis/web/websocket/ResultWebsocketService.java @@ -84,8 +84,8 @@ private void broadcastNewResultToParticipants(StudentParticipation studentPartic // Don't send students results after the exam ended boolean isAfterExamEnd = isWorkingPeriodOver && exercise.isExamExercise() && !exercise.getExam().isTestExam(); // If the assessment due date is not over yet, do not send manual feedback to students! - boolean isAutomaticAssessmentOrDueDateOver = AssessmentType.AUTOMATIC == result.getAssessmentType() || exercise.getAssessmentDueDate() == null - || ZonedDateTime.now().isAfter(exercise.getAssessmentDueDate()); + boolean isAutomaticAssessmentOrDueDateOver = AssessmentType.AUTOMATIC == result.getAssessmentType() || AssessmentType.AUTOMATIC_ATHENA == result.getAssessmentType() + || exercise.getAssessmentDueDate() == null || ZonedDateTime.now().isAfter(exercise.getAssessmentDueDate()); if (isAutomaticAssessmentOrDueDateOver && !isAfterExamEnd) { var students = studentParticipation.getStudents(); diff --git a/src/main/webapp/app/entities/submission.model.ts b/src/main/webapp/app/entities/submission.model.ts index ced9e0276d88..3dd3c6039acd 100644 --- a/src/main/webapp/app/entities/submission.model.ts +++ b/src/main/webapp/app/entities/submission.model.ts @@ -2,6 +2,7 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import { Participation } from 'app/entities/participation/participation.model'; import { Result } from 'app/entities/result.model'; import dayjs from 'dayjs/esm'; +import { AssessmentType } from 'app/entities/assessment-type.model'; export const enum SubmissionType { MANUAL = 'MANUAL', @@ -63,14 +64,14 @@ export function getLatestSubmissionResult(submission: Submission | undefined): R /** * Used to access a submissions result for a specific correctionRound - * + * Athena Results need to be excluded to avoid an assessment being locked by null * @param submission * @param correctionRound * @returns the results or undefined if submission or the result for the requested correctionRound is undefined */ export function getSubmissionResultByCorrectionRound(submission: Submission | undefined, correctionRound: number): Result | undefined { - if (submission?.results && submission?.results.length >= correctionRound) { - return submission.results[correctionRound]; + if (submission?.results && submission?.results.filter((result) => result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA).length >= correctionRound) { + return submission.results.filter((result) => result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA)[correctionRound]; } return undefined; } diff --git a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts index b518bd05bb5a..c2642528bc00 100644 --- a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts @@ -171,8 +171,16 @@ export class ExerciseScoresComponent implements OnInit, OnDestroy { // the result of the first correction round will be at index 0, // the result of a complaints or the second correction at index 1. participation.results?.sort((result1, result2) => (result1.id ?? 0) - (result2.id ?? 0)); - if (participation.results?.[0].submission) { - participation.submissions = [participation.results?.[0].submission]; + + const resultsWithoutAthena = participation.results?.filter((result) => result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA); + if (resultsWithoutAthena?.length != 0) { + if (resultsWithoutAthena?.[0].submission) { + participation.submissions = [resultsWithoutAthena?.[0].submission]; + } else if (participation.results?.[0].submission) { + participation.submissions = [participation.results?.[0].submission]; + } + } else { + participation.results = undefined; } }); this.filteredParticipations = this.filterByScoreRange(this.participations); diff --git a/src/main/webapp/app/exercises/shared/exercise-scores/manage-assessment-buttons.component.html b/src/main/webapp/app/exercises/shared/exercise-scores/manage-assessment-buttons.component.html index 4417caebec48..614a4d3b4ff3 100644 --- a/src/main/webapp/app/exercises/shared/exercise-scores/manage-assessment-buttons.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-scores/manage-assessment-buttons.component.html @@ -3,7 +3,9 @@ @if ( (correctionRound === 0 || participation.results?.[correctionRound - 1]?.completionDate) && (newManualResultAllowed || - (participation.results?.[correctionRound]?.assessmentType && participation.results?.[correctionRound]?.assessmentType !== AssessmentType.AUTOMATIC)) + (participation.results?.[correctionRound]?.assessmentType && + participation.results?.[correctionRound]?.assessmentType !== AssessmentType.AUTOMATIC && + participation.results?.[correctionRound]?.assessmentType !== AssessmentType.AUTOMATIC_ATHENA)) ) {

    + @if (this.exercise.type === ExerciseType.TEXT) { +
    + + + +
    + } @if (!!this.exercise.feedbackSuggestionModule) {
    diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts index 40ee934e6dc8..993b4154c449 100644 --- a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts @@ -15,6 +15,8 @@ export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit, OnCha @Input() dueDate?: dayjs.Dayjs; @Input() readOnly: boolean = false; + protected readonly ExerciseType = ExerciseType; + protected readonly AssessmentType = AssessmentType; readonly assessmentType: AssessmentType; @@ -72,10 +74,20 @@ export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit, OnCha if (event.target.checked) { this.exercise.feedbackSuggestionModule = this.availableAthenaModules.first(); } else { + this.exercise.allowFeedbackRequests = false; this.exercise.feedbackSuggestionModule = undefined; } } + toggleFeedbackRequests(event: any) { + if (event.target.checked) { + this.exercise.feedbackSuggestionModule = this.availableAthenaModules.first(); + this.exercise.allowFeedbackRequests = true; + } else { + this.exercise.allowFeedbackRequests = false; + } + } + private hasDueDatePassed() { return dayjs(this.exercise.dueDate).isBefore(dayjs()); } diff --git a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html index 5d41e4bfe798..a0de0676f7dc 100644 --- a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html +++ b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html @@ -84,7 +84,7 @@

    } - @if (result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA && showScoreChart && result.participation?.exercise) { + @if (showScoreChart && result.participation?.exercise) {
    {{ resultString }} diff --git a/src/main/webapp/app/exercises/shared/result/result.component.ts b/src/main/webapp/app/exercises/shared/result/result.component.ts index b323a640b21e..3415021e11c7 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/result.component.ts @@ -43,6 +43,7 @@ export class ResultComponent implements OnInit, OnChanges, OnDestroy { readonly ExerciseType = ExerciseType; readonly roundScoreSpecifiedByCourseSettings = roundValueSpecifiedByCourseSettings; readonly getCourseFromExercise = getCourseFromExercise; + protected readonly AssessmentType = AssessmentType; @Input() participation: Participation; @Input() isBuilding: boolean; diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index 800fe1b4aecd..2fd62aa574b7 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -94,6 +94,9 @@ export class ResultService implements IResultService { const relativeScore = roundValueSpecifiedByCourseSettings(result.score!, getCourseFromExercise(exercise)); const points = roundValueSpecifiedByCourseSettings((result.score! * exercise.maxPoints!) / 100, getCourseFromExercise(exercise)); if (exercise.type !== ExerciseType.PROGRAMMING) { + if (Result.isAthenaAIResult(result)) { + return this.getResultStringNonProgrammingExerciseWithAIFeedback(result, relativeScore, points, short); + } return this.getResultStringNonProgrammingExercise(relativeScore, points, short); } else { return this.getResultStringProgrammingExercise(result, exercise as ProgrammingExercise, relativeScore, points, short); @@ -101,7 +104,23 @@ export class ResultService implements IResultService { } /** - * Generates the result string for a programming exercise. Contains the score and points + * Generates the result string for a text exercise. Contains the score and points + * @param result the result object + * @param relativeScore the achieved score in percent + * @param points the amount of achieved points + * @param short flag that indicates if the resultString should use the short format + */ + private getResultStringNonProgrammingExerciseWithAIFeedback(result: Result, relativeScore: number, points: number, short: boolean | undefined): string { + let aiFeedbackMessage: string = ''; + if (result && Result.isAthenaAIResult(result) && result.successful === undefined) { + return this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackInProgress'); + } + aiFeedbackMessage = this.getResultStringNonProgrammingExercise(relativeScore, points, short); + return `${aiFeedbackMessage} (${this.translateService.instant('artemisApp.result.preliminary')})`; + } + + /** + * Generates the result string for a non programming exercise. Contains the score and points * @param relativeScore the achieved score in percent * @param points the amount of achieved points * @param short flag that indicates if the resultString should use the short format diff --git a/src/main/webapp/app/exercises/shared/result/result.utils.ts b/src/main/webapp/app/exercises/shared/result/result.utils.ts index b8b43ed63c80..e8ebbf03e898 100644 --- a/src/main/webapp/app/exercises/shared/result/result.utils.ts +++ b/src/main/webapp/app/exercises/shared/result/result.utils.ts @@ -10,6 +10,7 @@ import { ProgrammingSubmission } from 'app/entities/programming/programming-subm import { AssessmentType } from 'app/entities/assessment-type.model'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { faCheckCircle, faQuestionCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'; import { isModelingOrTextOrFileUpload, isParticipationInDueTime, isProgrammingOrQuiz } from 'app/exercises/shared/participation/participation.utils'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; @@ -165,8 +166,11 @@ export const evaluateTemplateStatus = ( if (inDueTime && initializedResultWithScore(result)) { // Submission is in due time of exercise and has a result with score - if (!assessmentDueDate || assessmentDueDate.isBefore(dayjs())) { - // the assessment due date has passed (or there was none) + if (!assessmentDueDate || assessmentDueDate.isBefore(dayjs()) || !isManualResult(result)) { + // the assessment due date has passed (or there was none) (or it is not manual feedback) + if (result?.assessmentType === AssessmentType.AUTOMATIC_ATHENA && result?.successful === undefined) { + return ResultTemplateStatus.IS_GENERATING_FEEDBACK; + } return ResultTemplateStatus.HAS_RESULT; } else { // the assessment period is still active @@ -177,7 +181,7 @@ export const evaluateTemplateStatus = ( if (!dueDate || dueDate.isSameOrAfter(dayjs())) { // the due date is in the future (or there is none) => the exercise is still ongoing return ResultTemplateStatus.SUBMITTED; - } else if (!assessmentDueDate || assessmentDueDate.isSameOrAfter(dayjs())) { + } else if (!assessmentDueDate || assessmentDueDate.isSameOrAfter(dayjs()) || isManualResult(result)) { // the due date is over, further submissions are no longer possible, waiting for grading return ResultTemplateStatus.SUBMITTED_WAITING_FOR_GRADING; } else { @@ -185,7 +189,7 @@ export const evaluateTemplateStatus = ( // TODO why is this distinct from the case above? The submission can still be graded and often is. return ResultTemplateStatus.NO_RESULT; } - } else if (initializedResultWithScore(result) && (!assessmentDueDate || assessmentDueDate.isBefore(dayjs()))) { + } else if (initializedResultWithScore(result) && (!assessmentDueDate || assessmentDueDate.isBefore(dayjs()) || !isManualResult(result))) { // Submission is not in due time of exercise, has a result with score and there is no assessmentDueDate for the exercise or it lies in the past. // TODO handle external submissions with new status "External" return ResultTemplateStatus.LATE; @@ -243,6 +247,13 @@ export const getTextColorClass = (result: Result | undefined, templateStatus: Re return 'text-secondary'; } + if (result.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { + if (result.successful == undefined) { + return 'text-primary'; + } + return 'text-secondary'; + } + if (templateStatus === ResultTemplateStatus.LATE) { return 'result-late'; } @@ -283,6 +294,13 @@ export const getResultIconClass = (result: Result | undefined, templateStatus: R return faQuestionCircle; } + if (result.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { + if (result.successful === undefined) { + return faCircleNotch; + } + return faQuestionCircle; + } + if (isBuildFailedAndResultIsAutomatic(result) || isAIResultAndFailed(result)) { return faTimesCircle; } @@ -309,9 +327,14 @@ export const getResultIconClass = (result: Result | undefined, templateStatus: R * @param result the result. It must include a participation and exercise. */ export const resultIsPreliminary = (result: Result) => { - return ( - result.participation && isProgrammingExerciseStudentParticipation(result.participation) && isResultPreliminary(result, result.participation.exercise as ProgrammingExercise) - ); + if (result.participation && result.participation.exercise && result.participation.exercise.type === ExerciseType.TEXT) { + return result.assessmentType === AssessmentType.AUTOMATIC_ATHENA; + } else + return ( + result.participation && + isProgrammingExerciseStudentParticipation(result.participation) && + isResultPreliminary(result, result.participation.exercise as ProgrammingExercise) + ); }; /** @@ -346,7 +369,7 @@ export const isBuildFailed = (submission?: Submission) => { * @param result the result. */ export const isManualResult = (result?: Result) => { - return result?.assessmentType !== AssessmentType.AUTOMATIC; + return result?.assessmentType !== AssessmentType.AUTOMATIC && result?.assessmentType !== AssessmentType.AUTOMATIC_ATHENA; }; /** diff --git a/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts b/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts index d996953e4cf6..492d620afe9c 100644 --- a/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts +++ b/src/main/webapp/app/exercises/text/assess/text-assessment.service.ts @@ -152,7 +152,7 @@ export class TextAssessmentService { if (participation.exercise) { this.accountService.setAccessRightsForExercise(participation.exercise); } - const submission = participation.submissions![0]; + const submission = participation.submissions!.last()!; let result; if (resultId) { result = getSubmissionResultById(submission, resultId); diff --git a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts index 875555a1492c..ed71e52c21d4 100644 --- a/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts +++ b/src/main/webapp/app/exercises/text/assess/text-submission-assessment.component.ts @@ -187,7 +187,7 @@ export class TextSubmissionAssessmentComponent extends TextAssessmentBaseCompone } this.participation = studentParticipation; - this.submission = this.participation!.submissions![0] as TextSubmission; + this.submission = this.participation?.submissions?.last() as TextSubmission; this.exercise = this.participation?.exercise as TextExercise; this.course = getCourseFromExercise(this.exercise); setLatestSubmissionResult(this.submission, getLatestSubmissionResult(this.submission)); diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts index 4199461314b2..e758013e44ee 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts @@ -106,6 +106,7 @@ export class TextExerciseDetailComponent implements OnInit, OnDestroy { details: [ ...defaultGradingDetails, { type: DetailType.Boolean, title: 'artemisApp.exercise.feedbackSuggestionsEnabled', data: { boolean: !!exercise.feedbackSuggestionModule } }, + { type: DetailType.Boolean, title: 'artemisApp.programmingExercise.timeline.manualFeedbackRequests', data: { boolean: exercise.allowFeedbackRequests } }, ...gradingInstructionsCriteriaDetails, ], }, diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.component.html b/src/main/webapp/app/exercises/text/participate/text-editor.component.html index 280323741cda..6a0d7d189269 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.component.html +++ b/src/main/webapp/app/exercises/text/participate/text-editor.component.html @@ -49,7 +49,7 @@ >
    - @if (!result) { + @if (!result || isAutomaticResult) {