@@ -2,7 +2,9 @@ import { NgIf } from '@angular/common';
22import {
33 Component ,
44 Inject ,
5+ OnDestroy ,
56 OnInit ,
7+ Optional ,
68} from '@angular/core' ;
79import {
810 FormsModule ,
@@ -15,36 +17,63 @@ import {
1517 TranslateModule ,
1618 TranslateService ,
1719} from '@ngx-translate/core' ;
18- import { take } from 'rxjs/operators' ;
20+ import {
21+ BehaviorSubject ,
22+ combineLatest ,
23+ Observable ,
24+ of ,
25+ Subscription ,
26+ } from 'rxjs' ;
27+ import {
28+ map ,
29+ startWith ,
30+ switchMap ,
31+ take ,
32+ } from 'rxjs/operators' ;
1933
2034import { getHomePageRoute } from '../../../app-routing-paths' ;
2135import { AuthService } from '../../../core/auth/auth.service' ;
2236import { RemoteData } from '../../../core/data/remote-data' ;
2337import { EPerson } from '../../../core/eperson/models/eperson.model' ;
2438import { FeedbackDataService } from '../../../core/feedback/feedback-data.service' ;
39+ import {
40+ CAPTCHA_NAME ,
41+ GoogleRecaptchaService ,
42+ } from '../../../core/google-recaptcha/google-recaptcha.service' ;
2543import { RouteService } from '../../../core/services/route.service' ;
2644import {
2745 NativeWindowRef ,
2846 NativeWindowService ,
2947} from '../../../core/services/window.service' ;
3048import { NoContent } from '../../../core/shared/NoContent.model' ;
31- import { getFirstCompletedRemoteData } from '../../../core/shared/operators' ;
49+ import {
50+ getFirstCompletedRemoteData ,
51+ getFirstSucceededRemoteDataPayload ,
52+ } from '../../../core/shared/operators' ;
3253import { URLCombiner } from '../../../core/url-combiner/url-combiner' ;
3354import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive' ;
55+ import { AlertComponent } from '../../../shared/alert/alert.component' ;
56+ import { AlertType } from '../../../shared/alert/alert-type' ;
3457import { ErrorComponent } from '../../../shared/error/error.component' ;
58+ import { GoogleRecaptchaComponent } from '../../../shared/google-recaptcha/google-recaptcha.component' ;
3559import { NotificationsService } from '../../../shared/notifications/notifications.service' ;
60+ import { ConfigurationDataService } from '../../../core/data/configuration-data.service' ;
61+ import { CookieService } from '../../../core/services/cookie.service' ;
62+ import { isNotEmpty } from '../../../shared/empty.util' ;
63+ import { AsyncPipe } from '@angular/common' ;
64+ import { KlaroService } from 'src/app/shared/cookies/klaro.service' ;
3665
3766@Component ( {
3867 selector : 'ds-base-feedback-form' ,
3968 templateUrl : './feedback-form.component.html' ,
4069 styleUrls : [ './feedback-form.component.scss' ] ,
4170 standalone : true ,
42- imports : [ FormsModule , ReactiveFormsModule , NgIf , ErrorComponent , TranslateModule , BtnDisabledDirective ] ,
71+ imports : [ FormsModule , ReactiveFormsModule , NgIf , ErrorComponent , TranslateModule , BtnDisabledDirective , AlertComponent , GoogleRecaptchaComponent , AsyncPipe ] ,
4372} )
4473/**
4574 * Component displaying the contents of the Feedback Statement
4675 */
47- export class FeedbackFormComponent implements OnInit {
76+ export class FeedbackFormComponent implements OnInit , OnDestroy {
4877
4978 /**
5079 * Form builder created used from the feedback from
@@ -55,6 +84,31 @@ export class FeedbackFormComponent implements OnInit {
5584 page : [ '' ] ,
5685 } ) ;
5786
87+ /**
88+ * Return true if the user completed the reCaptcha verification (checkbox mode)
89+ */
90+ checkboxCheckedSubject$ = new BehaviorSubject < boolean > ( false ) ;
91+
92+ disableUntilChecked = true ;
93+
94+ captchaVersion$ : Observable < string > ;
95+
96+ captchaMode$ : Observable < string > ;
97+
98+ /**
99+ * Whether reCAPTCHA verification is enabled for feedback
100+ */
101+ feedbackRecaptchaEnabled = false ;
102+
103+ /**
104+ * AlertType enumeration
105+ */
106+ public AlertTypeEnum = AlertType ;
107+
108+ subscriptions : Subscription [ ] = [ ] ;
109+
110+ readonly FEEDBACK_VERIFICATION_PROP_KEY = 'feedback.verification.enabled' ;
111+
58112 constructor (
59113 @Inject ( NativeWindowService ) protected _window : NativeWindowRef ,
60114 public routeService : RouteService ,
@@ -63,13 +117,20 @@ export class FeedbackFormComponent implements OnInit {
63117 protected translate : TranslateService ,
64118 private feedbackDataService : FeedbackDataService ,
65119 private authService : AuthService ,
66- private router : Router ) {
120+ private router : Router ,
121+ public googleRecaptchaService : GoogleRecaptchaService ,
122+ private configService : ConfigurationDataService ,
123+ private cookieService : CookieService ,
124+ @Optional ( ) public klaroService : KlaroService ,
125+ ) {
67126 }
68127
69128 /**
70129 * On init check if user is logged in and use its email if so
71130 */
72131 ngOnInit ( ) {
132+ this . captchaVersion$ = this . googleRecaptchaService . captchaVersion ( ) ;
133+ this . captchaMode$ = this . googleRecaptchaService . captchaMode ( ) ;
73134
74135 this . authService . getAuthenticatedUserFromStore ( ) . pipe ( take ( 1 ) ) . subscribe ( ( user : EPerson ) => {
75136 if ( user ) {
@@ -85,14 +146,79 @@ export class FeedbackFormComponent implements OnInit {
85146 this . feedbackForm . patchValue ( { page : relatedUrl } ) ;
86147 } ) ;
87148
149+ this . subscriptions . push ( this . configService . findByPropertyName ( this . FEEDBACK_VERIFICATION_PROP_KEY ) . pipe (
150+ getFirstSucceededRemoteDataPayload ( ) ,
151+ map ( ( res ) => res ?. values [ 0 ] . toLowerCase ( ) === 'true' ) ,
152+ take ( 1 ) ,
153+ ) . subscribe ( ( enabled : boolean ) => {
154+ this . feedbackRecaptchaEnabled = enabled ;
155+ if ( enabled ) {
156+ this . refreshRecaptchaScript ( ) ;
157+ this . subscriptions . push ( this . disableUntilCheckedFcn ( ) . subscribe ( ( res ) => {
158+ this . disableUntilChecked = res ;
159+ } ) ) ;
160+ }
161+ } ) ) ;
162+ }
163+
164+ ngOnDestroy ( ) : void {
165+ this . subscriptions . forEach ( ( sub : Subscription ) => sub . unsubscribe ( ) ) ;
166+ }
167+
168+ /**
169+ * Try to load the reCAPTCHA script if not already loaded via registration config
170+ */
171+ private refreshRecaptchaScript ( ) : void {
172+ if ( this . _window . nativeWindow . refreshCaptchaScript ) {
173+ this . _window . nativeWindow . refreshCaptchaScript ( ) ;
174+ } else {
175+ this . googleRecaptchaService . loadRecaptchaProperties ( ) . subscribe ( ) ;
176+ }
88177 }
89178
90179 /**
91180 * Function to create the feedback from form values
92181 */
93- createFeedback ( ) : void {
182+ createFeedback ( captchaToken ?: string ) : void {
183+ if ( this . feedbackRecaptchaEnabled ) {
184+ combineLatest ( [ this . captchaVersion$ , this . captchaMode$ ] ) . pipe (
185+ switchMap ( ( [ captchaVersion , captchaMode ] ) => {
186+ if ( captchaVersion === 'v3' ) {
187+ return this . googleRecaptchaService . getRecaptchaToken ( 'feedback' ) ;
188+ } else if ( captchaVersion === 'v2' && captchaMode === 'checkbox' ) {
189+ return of ( this . googleRecaptchaService . getRecaptchaTokenResponse ( ) ) ;
190+ } else if ( captchaVersion === 'v2' && captchaMode === 'invisible' ) {
191+ return of ( captchaToken ) ;
192+ } else {
193+ this . showNotification ( 'error' ) ;
194+ return of ( null ) ;
195+ }
196+ } ) ,
197+ take ( 1 ) ,
198+ ) . subscribe ( ( token ) => {
199+ if ( isNotEmpty ( token ) ) {
200+ this . submitFeedback ( token ) ;
201+ } else {
202+ this . showNotification ( 'error' ) ;
203+ }
204+ } ) ;
205+ } else {
206+ this . submitFeedback ( ) ;
207+ }
208+ }
209+
210+ /**
211+ * Send the feedback with the captcha token
212+ */
213+ private submitFeedback ( captcha ?: string ) : void {
94214 const url = this . feedbackForm . value . page . replace ( this . _window . nativeWindow . origin , '' ) ;
95- this . feedbackDataService . create ( this . feedbackForm . value ) . pipe ( getFirstCompletedRemoteData ( ) ) . subscribe ( ( response : RemoteData < NoContent > ) => {
215+ const feedback = { ...this . feedbackForm . value } ;
216+ if ( isNotEmpty ( captcha ) ) {
217+ feedback . captcha = captcha ;
218+ }
219+ this . feedbackDataService . create ( feedback ) . pipe (
220+ getFirstCompletedRemoteData ( ) ,
221+ ) . subscribe ( ( response : RemoteData < NoContent > ) => {
96222 if ( response . isSuccess ) {
97223 this . notificationsService . success ( this . translate . instant ( 'info.feedback.create.success' ) ) ;
98224 this . feedbackForm . reset ( ) ;
@@ -101,4 +227,63 @@ export class FeedbackFormComponent implements OnInit {
101227 } ) ;
102228 }
103229
230+ /**
231+ * Return true if the user has accepted the required cookies for reCaptcha
232+ */
233+ isRecaptchaCookieAccepted ( ) : boolean {
234+ const orejimeAnonymousCookie = this . cookieService . get ( 'orejime-anonymous' ) ;
235+ return isNotEmpty ( orejimeAnonymousCookie ) ? orejimeAnonymousCookie [ CAPTCHA_NAME ] : false ;
236+ }
237+
238+ /**
239+ * Return true if the user has not completed the reCaptcha verification (checkbox mode)
240+ */
241+ disableUntilCheckedFcn ( ) : Observable < boolean > {
242+ const checked$ = this . checkboxCheckedSubject$ . asObservable ( ) ;
243+ return combineLatest ( [ this . captchaVersion$ , this . captchaMode$ , checked$ ] ) . pipe (
244+ switchMap ( ( [ captchaVersion , captchaMode , checked ] ) =>
245+ captchaVersion === 'v2' && captchaMode === 'checkbox' ? of ( ! checked ) : of ( false ) ,
246+ ) ,
247+ startWith ( true ) ,
248+ ) ;
249+ }
250+
251+ onCheckboxChecked ( checked : boolean ) {
252+ this . checkboxCheckedSubject$ . next ( checked ) ;
253+ }
254+
255+ /**
256+ * execute the captcha function for v2 invisible
257+ */
258+ executeRecaptcha ( ) {
259+ this . googleRecaptchaService . executeRecaptcha ( ) ;
260+ }
261+
262+ /**
263+ * Open the cookie settings dialog
264+ */
265+ openCookieSettings ( ) : void {
266+ if ( this . klaroService ) {
267+ this . klaroService . showSettings ( ) ;
268+ }
269+ }
270+
271+ /**
272+ * Show a notification to the user
273+ */
274+ showNotification ( key : string ) {
275+ const notificationTitle = this . translate . instant ( 'info.feedback.google-recaptcha.notification.title' ) ;
276+ const notificationErrorMsg = this . translate . instant ( 'info.feedback.google-recaptcha.notification.message.error' ) ;
277+ const notificationExpiredMsg = this . translate . instant ( 'info.feedback.google-recaptcha.notification.message.expired' ) ;
278+ switch ( key ) {
279+ case 'expired' :
280+ this . notificationsService . warning ( notificationTitle , notificationExpiredMsg ) ;
281+ break ;
282+ case 'error' :
283+ this . notificationsService . error ( notificationTitle , notificationErrorMsg ) ;
284+ break ;
285+ default :
286+ console . warn ( `Unimplemented notification '${ key } ' from reCaptcha service` ) ;
287+ }
288+ }
104289}
0 commit comments