99
1010use Exception ;
1111use OC \App \CompareVersion ;
12- use OCA \DAV \CardDAV \CardDavBackend ;
1312use OCA \Contacts \AppInfo \Application ;
1413use OCA \Contacts \Db \FederatedInvite ;
1514use OCA \Contacts \Db \FederatedInviteMapper ;
1615use OCA \Contacts \IWayfProvider ;
1716use OCA \Contacts \Service \FederatedInvitesService ;
1817use OCA \Contacts \Service \GroupSharingService ;
1918use OCA \Contacts \Service \SocialApiService ;
19+ use OCA \DAV \CardDAV \CardDavBackend ;
2020use OCA \FederatedFileSharing \AddressHandler ;
2121use OCP \App \IAppManager ;
2222use OCP \AppFramework \Db \DoesNotExistException ;
4747
4848/**
4949 * Controller for federated invites related routes.
50- *
50+ *
5151 */
5252
53- class FederatedInvitesController extends PageController
54- {
53+ class FederatedInvitesController extends PageController {
5554 public function __construct (
5655 IRequest $ request ,
5756 private AddressHandler $ addressHandler ,
@@ -94,7 +93,7 @@ public function __construct(
9493
9594 /**
9695 * Returns all open (not yet accepted) invites.
97- *
96+ *
9897 * @return JSONResponse
9998 */
10099 #[NoAdminRequired]
@@ -105,7 +104,7 @@ public function getInvites(): JSONResponse {
105104 foreach ($ _invites as $ invite ) {
106105 if ($ invite instanceof FederatedInvite) {
107106 array_push (
108- $ invites ,
107+ $ invites ,
109108 $ invite ->jsonSerialize ()
110109 );
111110 }
@@ -122,15 +121,15 @@ public function getInvites(): JSONResponse {
122121 #[NoAdminRequired]
123122 #[NoCSRFRequired]
124123 public function deleteInvite (string $ token ): JSONResponse {
125- if (!isset ($ token )) {
124+ if (!isset ($ token )) {
126125 return new JSONResponse (['message ' => 'Token is required ' ], Http::STATUS_BAD_REQUEST );
127126 }
128127 try {
129128 $ uid = $ this ->userSession ->getUser ()->getUID ();
130129 $ invite = $ this ->federatedInviteMapper ->findInviteByTokenAndUidd ($ token , $ uid );
131130 $ this ->federatedInviteMapper ->delete ($ invite );
132131 return new JSONResponse (['token ' => $ token ], Http::STATUS_OK );
133- } catch (DoesNotExistException $ e ) {
132+ } catch (DoesNotExistException $ e ) {
134133 $ this ->logger ->error ("Could not find invite with token= $ token for user with uid= $ uid . Stacktrace: " . $ e ->getTraceAsString (), ['app ' => Application::APP_ID ]);
135134 return new JSONResponse (['message ' => 'An unexpected error occurred trying to delete the invite ' ], Http::STATUS_NOT_FOUND );
136135 } catch (Exception $ e ) {
@@ -141,32 +140,32 @@ public function deleteInvite(string $token): JSONResponse {
141140
142141 /**
143142 * Sets the token and provider states which triggers display of the invite accept dialog.
144- *
143+ *
145144 * @param string $token
146- * @param string $provider
145+ * @param string $providerDomain
147146 * @return TemplateResponse
148147 */
149148 #[NoAdminRequired]
150149 #[NoCSRFRequired]
151- public function inviteAcceptDialog (string $ token = "" , string $ provider = "" ): TemplateResponse {
150+ public function inviteAcceptDialog (string $ token = '' , string $ providerDomain = '' ): TemplateResponse {
152151 $ this ->initialStateService ->provideInitialState (Application::APP_ID , 'inviteToken ' , $ token );
153- $ this ->initialStateService ->provideInitialState (Application::APP_ID , 'inviteProvider ' , $ provider );
152+ $ this ->initialStateService ->provideInitialState (Application::APP_ID , 'inviteProvider ' , $ providerDomain );
154153 $ this ->initialStateService ->provideInitialState (Application::APP_ID , 'acceptInviteDialogUrl ' , FederatedInvitesService::OCM_INVITE_ACCEPT_DIALOG_ROUTE );
155154
156155 return $ this ->index ();
157156 }
158157
159158 /**
160159 * Creates an invitation to exchange contact info for the user with the specified uid.
161- *
160+ *
162161 * @param string $emailAddress the recipient email address to send the invitation to
163- * @param string $message the optional message to send with the invitation
162+ * @param string $message the optional message to send with the invitation
164163 * @return JSONResponse with data signature ['token' | 'message'] - the token of the invitation or an error message in case of error
165164 */
166165 #[NoAdminRequired]
167166 #[NoCSRFRequired]
168167 public function createInvite (string $ email , string $ message ): JSONResponse {
169- if (!isset ($ email )) {
168+ if (!isset ($ email )) {
170169 return new JSONResponse (['message ' => 'Recipient email is required ' ], Http::STATUS_BAD_REQUEST );
171170 }
172171
@@ -176,7 +175,7 @@ public function createInvite(string $email, string $message): JSONResponse {
176175 $ uid ,
177176 $ email ,
178177 );
179- if (count ($ existingInvites ) > 0 ) {
178+ if (count ($ existingInvites ) > 0 ) {
180179 $ this ->logger ->error ("An open invite already exists for user with uid $ uid and for recipient email $ email " , ['app ' => Application::APP_ID ]);
181180 return new JSONResponse (['message ' => $ this ->il10 ->t ('An open invite already exists. ' )], Http::STATUS_CONFLICT );
182181 }
@@ -194,18 +193,18 @@ public function createInvite(string $email, string $message): JSONResponse {
194193 $ invite ->setAccepted (false );
195194 try {
196195 $ this ->federatedInviteMapper ->insert ($ invite );
197- } catch (Exception $ e ) {
198- $ this ->logger ->error (" An unexpected error occurred saving a new invite. Stacktrace: " . $ e ->getTraceAsString (), ['app ' => Application::APP_ID ]);
196+ } catch (Exception $ e ) {
197+ $ this ->logger ->error (' An unexpected error occurred saving a new invite. Stacktrace: ' . $ e ->getTraceAsString (), ['app ' => Application::APP_ID ]);
199198 return new JSONResponse (['message ' => 'An unexpected error occurred creating the invite. ' ], Http::STATUS_NOT_FOUND );
200199 }
201200
202201 /** @var DataResponse */
203202 $ response = $ this ->sendEmail ($ token , $ email , $ message );
204- if ($ response ->getStatus () !== Http::STATUS_OK ) {
203+ if ($ response ->getStatus () !== Http::STATUS_OK ) {
205204 // delete invite in case sending the email has failed
206205 try {
207206 $ this ->federatedInviteMapper ->delete ($ invite );
208- } catch (Exception $ e ) {
207+ } catch (Exception $ e ) {
209208 $ this ->logger ->error ("An unexpected error occurred deleting invite with token $ token. Stacktrace: " . $ e ->getTraceAsString (), ['app ' => Application::APP_ID ]);
210209 return new JSONResponse (['message ' => 'An unexpected error occurred creating the invite. ' ], Http::STATUS_NOT_FOUND );
211210 }
@@ -221,15 +220,15 @@ public function createInvite(string $email, string $message): JSONResponse {
221220 /**
222221 * Accepts the invite and creates a new contact from the inviter.
223222 * On success the user is redirected to the new contact url.
224- *
223+ *
225224 * @param string $token the token of the invite
226- * @param string $provider the provider of the sender of the invite
225+ * @param string $provider the provider of the sender of the invite
227226 * @return JSONResponse with data signature ['contact' | 'message'] - the new contact url or an error message in case of error
228227 */
229228 #[NoAdminRequired]
230229 #[NoCSRFRequired]
231- public function inviteAccepted (string $ token = "" , string $ provider = "" ): JSONResponse {
232- if ($ token === "" || $ provider === "" ) {
230+ public function inviteAccepted (string $ token = '' , string $ provider = '' ): JSONResponse {
231+ if ($ token === '' || $ provider === '' ) {
233232 $ this ->logger ->error ("Both token and provider must be specified. Received: token= $ token, provider= $ provider " , ['app ' => Application::APP_ID ]);
234233 return new JSONResponse (['message ' => 'Both token and provider must be specified. ' ], Http::STATUS_NOT_FOUND );
235234 }
@@ -245,8 +244,8 @@ public function inviteAccepted(string $token = "", string $provider = ""): JSONR
245244 // TODO take provider as is, or do some verification ??
246245 "https:// $ provider/ocm/invite-accepted " ,
247246 [
248- 'body ' =>
249- [
247+ 'body '
248+ => [
250249 'recipientProvider ' => $ recipientProvider ,
251250 'token ' => $ token ,
252251 'userId ' => $ localUser ->getUID (),
@@ -261,10 +260,10 @@ public function inviteAccepted(string $token = "", string $provider = ""): JSONR
261260
262261 // Creating a contact does not return a specific 'contact already exists' error,
263262 // so we must check that explicitly
264- $ cloudId = $ data ['userID ' ] . " @ " . $ this ->addressHandler ->removeProtocolFromUrl ($ provider );
263+ $ cloudId = $ data ['userID ' ] . ' @ ' . $ this ->addressHandler ->removeProtocolFromUrl ($ provider );
265264 $ searchResult = $ this ->contactsManager ->search ($ cloudId , ['CLOUD ' ]);
266265 if (count ($ searchResult ) > 0 ) {
267- $ this ->logger ->info (" Contact with cloud id " . $ cloudId . " already exists. " , ['app ' => Application::APP_ID ]);
266+ $ this ->logger ->info (' Contact with cloud id ' . $ cloudId . ' already exists. ' , ['app ' => Application::APP_ID ]);
268267 return new JSONResponse (['message ' => "Contact with cloudID $ cloudId already exists. " ], Http::STATUS_CONFLICT );
269268 }
270269
@@ -279,15 +278,15 @@ public function inviteAccepted(string $token = "", string $provider = ""): JSONR
279278 $ this ->logger ->error ("Error accepting invite (token= $ token, provider= $ provider): Could not create new contact. " , ['app ' => Application::APP_ID ]);
280279 return new JSONResponse (['message ' => 'An unexpected error occurred trying to accept invite: could not create new contact ' ], Http::STATUS_NOT_FOUND );
281280 }
282- $ this ->logger ->info (" Created new contact with UID: " . $ newContact ['UID ' ] . " for user with UID: " . $ localUser ->getUID (), ['app ' => Application::APP_ID ]);
281+ $ this ->logger ->info (' Created new contact with UID: ' . $ newContact ['UID ' ] . ' for user with UID: ' . $ localUser ->getUID (), ['app ' => Application::APP_ID ]);
283282
284- $ contact = $ newContact ['UID ' ] . " ~ " . CardDavBackend::PERSONAL_ADDRESSBOOK_URI ;
283+ $ contact = $ newContact ['UID ' ] . ' ~ ' . CardDavBackend::PERSONAL_ADDRESSBOOK_URI ;
285284 $ url = $ this ->urlGenerator ->getAbsoluteURL (
286285 $ this ->urlGenerator ->linkToRoute ('contacts.page.index ' ) . $ this ->il10 ->t ('All contacts ' ) . '/ ' . $ contact
287286 );
288287 return new JSONResponse (['contact ' => $ url ], Http::STATUS_OK );
289288 } catch (\GuzzleHttp \Exception \RequestException $ e ) {
290- $ this ->logger ->error (" /invite-accepted returned an error: " . print_r ($ responseData , true ), ['app ' => Application::APP_ID ]);
289+ $ this ->logger ->error (' /invite-accepted returned an error: ' . print_r ($ responseData , true ), ['app ' => Application::APP_ID ]);
291290 /**
292291 * 400: Invalid or non existing token
293292 * 409: Invite already accepted
@@ -306,30 +305,85 @@ public function inviteAccepted(string $token = "", string $provider = ""): JSONR
306305 return new JSONResponse (['message ' => 'An unexpected error occurred trying to accept invite ' ], Http::STATUS_NOT_FOUND );
307306 }
308307 }
308+ /**
309+ * Do OCM discovery on behalf of VUE frontend to avoid CSRF issues
310+ * @param string $base base url to discover
311+ * @return DataResponse
312+ */
313+ #[PublicPage]
314+ #[NoCSRFRequired]
315+ public function discover (string $ base ): DataResponse {
316+ $ base = trim ($ base );
317+ if ($ base === '' ) {
318+ return new DataResponse (['error ' => 'empty base ' ], 400 );
319+ }
320+
321+ // normalize base
322+ if (!preg_match ('#^https?://#i ' , $ base )) {
323+ $ base = 'https:// ' . $ base ;
324+ }
325+ $ base = rtrim ($ base , '/ ' );
326+
327+ $ client = $ this ->httpClient ->newClient ([
328+ 'timeout ' => 5 ,
329+ 'connect_timeout ' => 5 ,
330+ 'allow_redirects ' => true ,
331+ ]);
332+
333+ foreach ([$ base . '/.well-known/ocm ' , $ base . '/ocm-provider ' ] as $ ep ) {
334+ try {
335+ $ resp = $ client ->get ($ ep , ['headers ' => ['Accept ' => 'application/json ' ]]);
336+ $ code = $ resp ->getStatusCode ();
337+ if ($ code >= 200 && $ code < 300 ) {
338+ $ data = json_decode ($ resp ->getBody (), true );
339+ if (is_array ($ data ) && !empty ($ data ['inviteAcceptDialog ' ])) {
340+ $ dialog = $ data ['inviteAcceptDialog ' ];
341+ $ absolute = preg_match ('#^https?://#i ' , $ dialog ) ? $ dialog : $ base . $ dialog ;
342+ return new DataResponse ([
343+ 'base ' => $ base ,
344+ 'inviteAcceptDialog ' => $ dialog ,
345+ 'inviteAcceptDialogAbsolute ' => $ absolute ,
346+ 'raw ' => $ data ,
347+ ]);
348+ }
349+ }
350+ } catch (\Throwable $ e ) {
351+ // try next endpoint
352+ }
353+ }
354+ return new DataResponse (['error ' => 'OCM discovery failed ' , 'base ' => $ base ], 404 );
355+ }
309356
310357 /**
311358 * Accepts the invite and creates a new contact from the inviter.
312359 * On success the user is redirected to the new contact url.
313- *
360+ *
314361 * @param string $token the token of the invite
315- * @param string $provider the provider of the sender of the invite
362+ * @param string $provider the provider of the sender of the invite
316363 * @return TemplateResponse the WAYF page
317364 */
318365 #[PublicPage]
319366 #[NoCSRFRequired]
320- public function wayf (string $ token = "" , string $ provider = "" ): TemplateResponse {
321- try {
367+ public function wayf (string $ token = '' ): TemplateResponse {
368+ Util::addScript (Application::APP_ID , 'contacts-wayf ' );
369+ Util::addStyle (Application::APP_ID , 'contacts-wayf ' );
370+ try {
322371 $ providers = $ this ->wayfProvider ->getMeshProviders ();
323- $ params = ['providers ' => $ providers , 'token ' => $ token , 'provider ' => $ provider ];
324- $ template = new TemplateResponse ('contacts ' , 'wayf ' , $ params , TemplateResponse::RENDER_AS_BLANK );
325- return $ template ;
372+ usort ($ providers , function ($ a , $ b ) {
373+ return strcmp ($ a ['name ' ], $ b ['name ' ]);
374+ });
375+ $ providerDomain = parse_url ($ this ->urlGenerator ->getBaseUrl (), PHP_URL_HOST );
376+ $ this ->initialStateService ->provideInitialState (Application::APP_ID , 'wayf ' , [
377+ 'providers ' => $ providers ,
378+ 'providerDomain ' => $ providerDomain ,
379+ 'token ' => $ token ,
380+ ]);
326381
327- } catch (Exception $ e ) {
328- $ this ->logger ->error ($ e ->getMessage () . ' Trace: ' . $ e ->getTraceAsString (), ['app ' => Application::APP_ID ]);
329- $ params = ['error ' => 'An error has occurred ' ];
330- $ template = new TemplateResponse ('contacts ' , 'wayf ' , $ params , TemplateResponse::RENDER_AS_BLANK );
331- return $ template ;
332- }
382+ } catch (Exception $ e ) {
383+ $ this ->logger ->error ($ e ->getMessage () . ' Trace: ' . $ e ->getTraceAsString (), ['app ' => Application::APP_ID ]);
384+ }
385+ $ template = new TemplateResponse ('contacts ' , 'wayf ' , [], TemplateResponse::RENDER_AS_GUEST );
386+ return $ template ;
333387 }
334388
335389 /**
@@ -341,7 +395,7 @@ public function wayf(string $token = "", string $provider = ""): TemplateRespons
341395 private function sendEmail (string $ token , string $ address , string $ message ): JSONResponse {
342396 /** @var IMessage */
343397 $ email = $ this ->mailer ->createMessage ();
344- if (!$ this ->mailer ->validateMailAddress ($ address )) {
398+ if (!$ this ->mailer ->validateMailAddress ($ address )) {
345399 $ this ->logger ->error ("Could not sent invite, invalid email address ' $ address' " , ['app ' => Application::APP_ID ]);
346400 return new JSONResponse (['message ' => 'Recipient email address is invalid ' ], Http::STATUS_NOT_FOUND );
347401 }
@@ -359,9 +413,8 @@ private function sendEmail(string $token, string $address, string $message): JSO
359413 );
360414 $ email ->setFrom ([Util::getDefaultEmailAddress ($ instanceName ) => $ senderName ]);
361415
362- $ fqdn = $ this ->federatedInvitesService ->getProviderFQDN ();
363416 $ wayfEndpoint = $ this ->wayfProvider ->getWayfEndpoint ();
364- $ inviteLink = "$ wayfEndpoint?token= $ token&provider= $ fqdn " ;
417+ $ inviteLink = "$ wayfEndpoint?token= $ token " ;
365418
366419 $ body = "$ message \nThe invite link: $ inviteLink " ;
367420 $ email ->setPlainBody ($ body );
0 commit comments