22import {
33 Component ,
44 Inject ,
5+ OnDestroy ,
56 OnInit ,
7+ Optional ,
68} from '@angular/core' ;
79import {
810 FormsModule ,
@@ -15,41 +17,71 @@ 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 { OrejimeService } from '../../../shared/cookies/orejime.service' ;
61+ import { ConfigurationDataService } from '../../../core/data/configuration-data.service' ;
62+ import { CookieService } from '../../../core/services/cookie.service' ;
63+ import { isNotEmpty } from '../../../shared/empty.util' ;
64+ import { AsyncPipe } from '@angular/common' ;
3665
3766@Component ( {
3867 selector : 'ds-base-feedback-form' ,
3968 templateUrl : './feedback-form.component.html' ,
4069 styleUrls : [ './feedback-form.component.scss' ] ,
4170 imports : [
71+ AlertComponent ,
4272 BtnDisabledDirective ,
4373 ErrorComponent ,
4474 FormsModule ,
75+ GoogleRecaptchaComponent ,
4576 ReactiveFormsModule ,
4677 TranslateModule ,
78+ AsyncPipe ,
4779 ] ,
4880} )
4981/**
5082 * Component displaying the contents of the Feedback Statement
5183 */
52- export class FeedbackFormComponent implements OnInit {
84+ export class FeedbackFormComponent implements OnInit , OnDestroy {
5385
5486 /**
5587 * Form builder created used from the feedback from
@@ -60,6 +92,31 @@ export class FeedbackFormComponent implements OnInit {
6092 page : [ '' ] ,
6193 } ) ;
6294
95+ /**
96+ * Return true if the user completed the reCaptcha verification (checkbox mode)
97+ */
98+ checkboxCheckedSubject$ = new BehaviorSubject < boolean > ( false ) ;
99+
100+ disableUntilChecked = true ;
101+
102+ captchaVersion$ : Observable < string > ;
103+
104+ captchaMode$ : Observable < string > ;
105+
106+ /**
107+ * Whether reCAPTCHA verification is enabled for feedback
108+ */
109+ feedbackRecaptchaEnabled = false ;
110+
111+ /**
112+ * AlertType enumeration
113+ */
114+ public AlertTypeEnum = AlertType ;
115+
116+ subscriptions : Subscription [ ] = [ ] ;
117+
118+ readonly FEEDBACK_VERIFICATION_PROP_KEY = 'feedback.verification.enabled' ;
119+
63120 constructor (
64121 @Inject ( NativeWindowService ) protected _window : NativeWindowRef ,
65122 public routeService : RouteService ,
@@ -68,13 +125,20 @@ export class FeedbackFormComponent implements OnInit {
68125 protected translate : TranslateService ,
69126 private feedbackDataService : FeedbackDataService ,
70127 private authService : AuthService ,
71- private router : Router ) {
128+ private router : Router ,
129+ public googleRecaptchaService : GoogleRecaptchaService ,
130+ private configService : ConfigurationDataService ,
131+ private cookieService : CookieService ,
132+ @Optional ( ) public orejimeService : OrejimeService ,
133+ ) {
72134 }
73135
74136 /**
75137 * On init check if user is logged in and use its email if so
76138 */
77139 ngOnInit ( ) {
140+ this . captchaVersion$ = this . googleRecaptchaService . captchaVersion ( ) ;
141+ this . captchaMode$ = this . googleRecaptchaService . captchaMode ( ) ;
78142
79143 this . authService . getAuthenticatedUserFromStore ( ) . pipe ( take ( 1 ) ) . subscribe ( ( user : EPerson ) => {
80144 if ( user ) {
@@ -90,14 +154,79 @@ export class FeedbackFormComponent implements OnInit {
90154 this . feedbackForm . patchValue ( { page : relatedUrl } ) ;
91155 } ) ;
92156
157+ this . subscriptions . push ( this . configService . findByPropertyName ( this . FEEDBACK_VERIFICATION_PROP_KEY ) . pipe (
158+ getFirstSucceededRemoteDataPayload ( ) ,
159+ map ( ( res ) => res ?. values [ 0 ] . toLowerCase ( ) === 'true' ) ,
160+ take ( 1 ) ,
161+ ) . subscribe ( ( enabled : boolean ) => {
162+ this . feedbackRecaptchaEnabled = enabled ;
163+ if ( enabled ) {
164+ this . refreshRecaptchaScript ( ) ;
165+ this . subscriptions . push ( this . disableUntilCheckedFcn ( ) . subscribe ( ( res ) => {
166+ this . disableUntilChecked = res ;
167+ } ) ) ;
168+ }
169+ } ) ) ;
170+ }
171+
172+ ngOnDestroy ( ) : void {
173+ this . subscriptions . forEach ( ( sub : Subscription ) => sub . unsubscribe ( ) ) ;
174+ }
175+
176+ /**
177+ * Try to load the reCAPTCHA script if not already loaded via registration config
178+ */
179+ private refreshRecaptchaScript ( ) : void {
180+ if ( this . _window . nativeWindow . refreshCaptchaScript ) {
181+ this . _window . nativeWindow . refreshCaptchaScript ( ) ;
182+ } else {
183+ this . googleRecaptchaService . loadRecaptchaProperties ( ) . subscribe ( ) ;
184+ }
93185 }
94186
95187 /**
96188 * Function to create the feedback from form values
97189 */
98- createFeedback ( ) : void {
190+ createFeedback ( captchaToken ?: string ) : void {
191+ if ( this . feedbackRecaptchaEnabled ) {
192+ combineLatest ( [ this . captchaVersion$ , this . captchaMode$ ] ) . pipe (
193+ switchMap ( ( [ captchaVersion , captchaMode ] ) => {
194+ if ( captchaVersion === 'v3' ) {
195+ return this . googleRecaptchaService . getRecaptchaToken ( 'feedback' ) ;
196+ } else if ( captchaVersion === 'v2' && captchaMode === 'checkbox' ) {
197+ return of ( this . googleRecaptchaService . getRecaptchaTokenResponse ( ) ) ;
198+ } else if ( captchaVersion === 'v2' && captchaMode === 'invisible' ) {
199+ return of ( captchaToken ) ;
200+ } else {
201+ this . showNotification ( 'error' ) ;
202+ return of ( null ) ;
203+ }
204+ } ) ,
205+ take ( 1 ) ,
206+ ) . subscribe ( ( token ) => {
207+ if ( isNotEmpty ( token ) ) {
208+ this . submitFeedback ( token ) ;
209+ } else {
210+ this . showNotification ( 'error' ) ;
211+ }
212+ } ) ;
213+ } else {
214+ this . submitFeedback ( ) ;
215+ }
216+ }
217+
218+ /**
219+ * Send the feedback with the captcha token
220+ */
221+ private submitFeedback ( captcha ?: string ) : void {
99222 const url = this . feedbackForm . value . page . replace ( this . _window . nativeWindow . origin , '' ) ;
100- this . feedbackDataService . create ( this . feedbackForm . value ) . pipe ( getFirstCompletedRemoteData ( ) ) . subscribe ( ( response : RemoteData < NoContent > ) => {
223+ const feedback = { ...this . feedbackForm . value } ;
224+ if ( isNotEmpty ( captcha ) ) {
225+ feedback . captcha = captcha ;
226+ }
227+ this . feedbackDataService . create ( feedback ) . pipe (
228+ getFirstCompletedRemoteData ( ) ,
229+ ) . subscribe ( ( response : RemoteData < NoContent > ) => {
101230 if ( response . isSuccess ) {
102231 this . notificationsService . success ( this . translate . instant ( 'info.feedback.create.success' ) ) ;
103232 this . feedbackForm . reset ( ) ;
@@ -106,4 +235,63 @@ export class FeedbackFormComponent implements OnInit {
106235 } ) ;
107236 }
108237
238+ /**
239+ * Return true if the user has accepted the required cookies for reCaptcha
240+ */
241+ isRecaptchaCookieAccepted ( ) : boolean {
242+ const orejimeAnonymousCookie = this . cookieService . get ( 'orejime-anonymous' ) ;
243+ return isNotEmpty ( orejimeAnonymousCookie ) ? orejimeAnonymousCookie [ CAPTCHA_NAME ] : false ;
244+ }
245+
246+ /**
247+ * Return true if the user has not completed the reCaptcha verification (checkbox mode)
248+ */
249+ disableUntilCheckedFcn ( ) : Observable < boolean > {
250+ const checked$ = this . checkboxCheckedSubject$ . asObservable ( ) ;
251+ return combineLatest ( [ this . captchaVersion$ , this . captchaMode$ , checked$ ] ) . pipe (
252+ switchMap ( ( [ captchaVersion , captchaMode , checked ] ) =>
253+ captchaVersion === 'v2' && captchaMode === 'checkbox' ? of ( ! checked ) : of ( false ) ,
254+ ) ,
255+ startWith ( true ) ,
256+ ) ;
257+ }
258+
259+ onCheckboxChecked ( checked : boolean ) {
260+ this . checkboxCheckedSubject$ . next ( checked ) ;
261+ }
262+
263+ /**
264+ * execute the captcha function for v2 invisible
265+ */
266+ executeRecaptcha ( ) {
267+ this . googleRecaptchaService . executeRecaptcha ( ) ;
268+ }
269+
270+ /**
271+ * Open the cookie settings dialog
272+ */
273+ openCookieSettings ( ) : void {
274+ if ( this . orejimeService ) {
275+ this . orejimeService . showSettings ( ) ;
276+ }
277+ }
278+
279+ /**
280+ * Show a notification to the user
281+ */
282+ showNotification ( key : string ) {
283+ const notificationTitle = this . translate . instant ( 'info.feedback.google-recaptcha.notification.title' ) ;
284+ const notificationErrorMsg = this . translate . instant ( 'info.feedback.google-recaptcha.notification.message.error' ) ;
285+ const notificationExpiredMsg = this . translate . instant ( 'info.feedback.google-recaptcha.notification.message.expired' ) ;
286+ switch ( key ) {
287+ case 'expired' :
288+ this . notificationsService . warning ( notificationTitle , notificationExpiredMsg ) ;
289+ break ;
290+ case 'error' :
291+ this . notificationsService . error ( notificationTitle , notificationErrorMsg ) ;
292+ break ;
293+ default :
294+ console . warn ( `Unimplemented notification '${ key } ' from reCaptcha service` ) ;
295+ }
296+ }
109297}
0 commit comments