1 /***************************************************************************
2     begin                : July 13, 2016
3     copyright            : (C) 2016 by Monsanto Company, USA
4     author               : Larry Shaffer, Boundless Spatial
5     email                : lshaffer at boundlessgeo dot com
6  ***************************************************************************
7  *                                                                         *
8  *   This program is free software; you can redistribute it and/or modify  *
9  *   it under the terms of the GNU General Public License as published by  *
10  *   the Free Software Foundation; either version 2 of the License, or     *
11  *   (at your option) any later version.                                   *
12  *                                                                         *
13  ***************************************************************************/
14 
15 #include "qgsauthoauth2edit.h"
16 #include "ui_qgsauthoauth2edit.h"
17 
18 #include <QDir>
19 #include <QFileDialog>
20 #include <QDesktopServices>
21 #include <QUrl>
22 
23 #include "Json.h"
24 
25 #include "qgsapplication.h"
26 #include "qgsauthguiutils.h"
27 #include "qgsauthmanager.h"
28 #include "qgsauthconfigedit.h"
29 #include "qgsmessagelog.h"
30 #include "qgsnetworkaccessmanager.h"
31 
QgsAuthOAuth2Edit(QWidget * parent)32 QgsAuthOAuth2Edit::QgsAuthOAuth2Edit( QWidget *parent )
33   : QgsAuthMethodEdit( parent )
34   , mDefinedConfigsCache( QgsStringMap() )
35 {
36   setupUi( this );
37 
38   initGui();
39 
40   initConfigObjs();
41 
42   populateGrantFlows();
43   updateGrantFlow( static_cast<int>( QgsAuthOAuth2Config::AuthCode ) ); // first index: Authorization Code
44 
45   populateAccessMethods();
46 
47   queryTableSelectionChanged();
48 
49   loadDefinedConfigs();
50 
51   setupConnections();
52 
53   loadFromOAuthConfig( mOAuthConfigCustom.get() );
54   updatePredefinedLocationsTooltip();
55 
56   pteDefinedDesc->setOpenLinks( false );
57   connect( pteDefinedDesc, &QTextBrowser::anchorClicked, this, [ = ]( const QUrl & url )
58   {
59     QDesktopServices::openUrl( url );
60   } );
61 }
62 
63 
initGui()64 void QgsAuthOAuth2Edit::initGui()
65 {
66   mParentName = parentNameField();
67 
68   frameNotify->setVisible( false );
69 
70   // TODO: add messagebar to notify frame?
71 
72   tabConfigs->setCurrentIndex( customTab() );
73 
74   btnExport->setEnabled( false );
75 
76   chkbxTokenPersist->setChecked( false );
77 
78   grpbxAdvanced->setCollapsed( true );
79   grpbxAdvanced->setFlat( false );
80 
81   btnTokenClear = new QToolButton( this );
82   btnTokenClear->setObjectName( QStringLiteral( "btnTokenClear" ) );
83   btnTokenClear->setMaximumHeight( 20 );
84   btnTokenClear->setText( tr( "Tokens" ) );
85   btnTokenClear->setToolTip( tr( "Remove cached tokens" ) );
86   btnTokenClear->setIcon( QIcon( QStringLiteral( ":/oauth2method/svg/close.svg" ) ) );
87   btnTokenClear->setIconSize( QSize( 12, 12 ) );
88   btnTokenClear->setToolButtonStyle( Qt::ToolButtonTextBesideIcon );
89   btnTokenClear->setEnabled( hasTokenCacheFile() );
90 
91   connect( btnTokenClear, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::removeTokenCacheFile );
92   tabConfigs->setCornerWidget( btnTokenClear, Qt::TopRightCorner );
93 }
94 
parentWidget() const95 QWidget *QgsAuthOAuth2Edit::parentWidget() const
96 {
97   if ( !window() )
98   {
99     return nullptr;
100   }
101 
102   const QMetaObject *metaObject = window()->metaObject();
103   const QString parentclass = metaObject->className();
104   //QgsDebugMsg( QStringLiteral( "parent class: %1" ).arg( parentclass ) );
105   if ( parentclass != QLatin1String( "QgsAuthConfigEdit" ) )
106   {
107     QgsDebugMsg( QStringLiteral( "Parent widget not QgsAuthConfigEdit instance" ) );
108     return nullptr;
109   }
110 
111   return window();
112 }
113 
parentNameField() const114 QLineEdit *QgsAuthOAuth2Edit::parentNameField() const
115 {
116   return parentWidget() ? parentWidget()->findChild<QLineEdit *>( QStringLiteral( "leName" ) ) : nullptr;
117 }
118 
parentConfigId() const119 QString QgsAuthOAuth2Edit::parentConfigId() const
120 {
121   if ( !parentWidget() )
122   {
123     return QString();
124   }
125 
126   QgsAuthConfigEdit *cie = qobject_cast<QgsAuthConfigEdit *>( parentWidget() );
127   if ( !cie )
128   {
129     QgsDebugMsg( QStringLiteral( "Could not cast to QgsAuthConfigEdit" ) );
130     return QString();
131   }
132 
133   if ( cie->configId().isEmpty() )
134   {
135     QgsDebugMsg( QStringLiteral( "QgsAuthConfigEdit->configId() is empty" ) );
136   }
137 
138   return cie->configId();
139 }
140 
141 
setupConnections()142 void QgsAuthOAuth2Edit::setupConnections()
143 {
144   // Action and interaction connections
145   connect( tabConfigs, &QTabWidget::currentChanged, this, &QgsAuthOAuth2Edit::tabIndexChanged );
146 
147   connect( btnExport, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::exportOAuthConfig );
148   connect( btnImport, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::importOAuthConfig );
149 
150   connect( tblwdgQueryPairs, &QTableWidget::itemSelectionChanged, this, &QgsAuthOAuth2Edit::queryTableSelectionChanged );
151 
152   connect( btnAddQueryPair, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::addQueryPair );
153   connect( btnRemoveQueryPair, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::removeQueryPair );
154 
155   connect( lstwdgDefinedConfigs, &QListWidget::currentItemChanged, this, &QgsAuthOAuth2Edit::currentDefinedItemChanged );
156 
157   connect( btnGetDefinedDirPath, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::getDefinedCustomDir );
158   connect( leDefinedDirPath, &QLineEdit::textChanged, this, &QgsAuthOAuth2Edit::definedCustomDirChanged );
159 
160   connect( btnSoftStatementDir, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::getSoftStatementDir );
161   connect( leSoftwareStatementJwtPath, &QLineEdit::textChanged, this, &QgsAuthOAuth2Edit::softwareStatementJwtPathChanged );
162   connect( leSoftwareStatementConfigUrl, &QLineEdit::textChanged, this, [ = ]( const QString & txt )
163   {
164     btnRegister->setEnabled( ! leSoftwareStatementJwtPath->text().isEmpty()
165                              && ( QUrl( txt ).isValid() || ! mRegistrationEndpoint.isEmpty() ) );
166   } );
167   connect( btnRegister, &QPushButton::clicked, this, &QgsAuthOAuth2Edit::getSoftwareStatementConfig );
168 
169   // Custom config editing connections
170   connect( cmbbxGrantFlow, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
171            this, &QgsAuthOAuth2Edit::updateGrantFlow ); // also updates GUI
172   connect( pteDescription, &QPlainTextEdit::textChanged, this, &QgsAuthOAuth2Edit::descriptionChanged );
173   connect( leRequestUrl, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setRequestUrl );
174   connect( leTokenUrl, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setTokenUrl );
175   connect( leRefreshTokenUrl, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setRefreshTokenUrl );
176   connect( leRedirectUrl, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setRedirectUrl );
177   connect( spnbxRedirectPort, static_cast<void ( QSpinBox::* )( int )>( &QSpinBox::valueChanged ),
178            mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setRedirectPort );
179   connect( leClientId, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setClientId );
180   connect( leClientSecret, &QgsPasswordLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setClientSecret );
181   connect( leUsername, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setUsername );
182   connect( lePassword, &QgsPasswordLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setPassword );
183   connect( leScope, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setScope );
184   connect( leApiKey, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setApiKey );
185   connect( chkbxTokenPersist, &QCheckBox::toggled, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setPersistToken );
186   connect( cmbbxAccessMethod, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
187            this, &QgsAuthOAuth2Edit::updateConfigAccessMethod );
188   connect( spnbxRequestTimeout, static_cast<void ( QSpinBox::* )( int )>( &QSpinBox::valueChanged ),
189            mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setRequestTimeout );
190 
191   connect( mTokenHeaderLineEdit, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setCustomHeader );
192 
193   connect( mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::validityChanged, this, &QgsAuthOAuth2Edit::configValidityChanged );
194 
195   if ( mParentName )
196   {
197     connect( mParentName, &QLineEdit::textChanged, this, &QgsAuthOAuth2Edit::configValidityChanged );
198   }
199 }
200 
201 
configValidityChanged()202 void QgsAuthOAuth2Edit::configValidityChanged()
203 {
204   validateConfig();
205   const bool parentname = mParentName && !mParentName->text().isEmpty();
206   btnExport->setEnabled( mValid && parentname );
207 }
208 
validateConfig()209 bool QgsAuthOAuth2Edit::validateConfig()
210 {
211   const bool curvalid = ( onCustomTab() ? mOAuthConfigCustom->isValid() : !mDefinedId.isEmpty() );
212   if ( mValid != curvalid )
213   {
214     mValid = curvalid;
215     emit validityChanged( curvalid );
216   }
217   return curvalid;
218 }
219 
configMap() const220 QgsStringMap QgsAuthOAuth2Edit::configMap() const
221 {
222   QgsStringMap configmap;
223   bool ok = false;
224 
225   if ( onCustomTab() )
226   {
227     if ( !mOAuthConfigCustom || !mOAuthConfigCustom->isValid() )
228     {
229       QgsDebugMsg( QStringLiteral( "FAILED to serialize OAuth config object: null or invalid object" ) );
230       return configmap;
231     }
232 
233     mOAuthConfigCustom->setQueryPairs( queryPairs() );
234 
235     const QByteArray configtxt = mOAuthConfigCustom->saveConfigTxt( QgsAuthOAuth2Config::JSON, false, &ok );
236 
237     if ( !ok )
238     {
239       QgsDebugMsg( QStringLiteral( "FAILED to serialize OAuth config object" ) );
240       return configmap;
241     }
242 
243     if ( configtxt.isEmpty() )
244     {
245       QgsDebugMsg( QStringLiteral( "FAILED to serialize OAuth config object: content empty" ) );
246       return configmap;
247     }
248 
249     //###################### DO NOT LEAVE ME UNCOMMENTED #####################
250     //QgsDebugMsg( QStringLiteral( "SAVE oauth2config configtxt: \n\n%1\n\n" ).arg( QString( configtxt ) ) );
251     //###################### DO NOT LEAVE ME UNCOMMENTED #####################
252 
253     configmap.insert( QStringLiteral( "oauth2config" ), QString( configtxt ) );
254 
255     updateTokenCacheFile( mOAuthConfigCustom->persistToken() );
256   }
257   else if ( onDefinedTab() && !mDefinedId.isEmpty() )
258   {
259     configmap.insert( QStringLiteral( "definedid" ), mDefinedId );
260     configmap.insert( QStringLiteral( "defineddirpath" ), leDefinedDirPath->text() );
261     configmap.insert( QStringLiteral( "querypairs" ),
262                       QgsAuthOAuth2Config::serializeFromVariant(
263                         queryPairs(), QgsAuthOAuth2Config::JSON, false ) );
264   }
265 
266   return configmap;
267 }
268 
loadConfig(const QgsStringMap & configmap)269 void QgsAuthOAuth2Edit::loadConfig( const QgsStringMap &configmap )
270 {
271   clearConfig();
272 
273   mConfigMap = configmap;
274   bool ok = false;
275 
276   //QgsDebugMsg( QStringLiteral( "oauth2config: " ).arg( configmap.value( QStringLiteral( "oauth2config" ) ) ) );
277 
278   if ( configmap.contains( QStringLiteral( "oauth2config" ) ) )
279   {
280     tabConfigs->setCurrentIndex( customTab() );
281     const QByteArray configtxt = configmap.value( QStringLiteral( "oauth2config" ) ).toUtf8();
282     if ( !configtxt.isEmpty() )
283     {
284       //###################### DO NOT LEAVE ME UNCOMMENTED #####################
285       //QgsDebugMsg( QStringLiteral( "LOAD oauth2config configtxt: \n\n%1\n\n" ).arg( QString( configtxt ) ) );
286       //###################### DO NOT LEAVE ME UNCOMMENTED #####################
287 
288       if ( !mOAuthConfigCustom->loadConfigTxt( configtxt, QgsAuthOAuth2Config::JSON ) )
289       {
290         QgsDebugMsg( QStringLiteral( "FAILED to load OAuth2 config into object" ) );
291       }
292 
293       //###################### DO NOT LEAVE ME UNCOMMENTED #####################
294       //QVariantMap vmap = mOAuthConfigCustom->mappedProperties();
295       //QByteArray vmaptxt = QgsAuthOAuth2Config::serializeFromVariant(vmap, QgsAuthOAuth2Config::JSON, true );
296       //QgsDebugMsg( QStringLiteral( "LOAD oauth2config vmaptxt: \n\n%1\n\n" ).arg( QString( vmaptxt ) ) );
297       //###################### DO NOT LEAVE ME UNCOMMENTED #####################
298 
299       // could only be loading defaults at this point
300       loadFromOAuthConfig( mOAuthConfigCustom.get() );
301 
302       mPrevPersistToken = mOAuthConfigCustom->persistToken();
303     }
304     else
305     {
306       QgsDebugMsg( QStringLiteral( "FAILED to load OAuth2 config: empty config txt" ) );
307     }
308   }
309   else if ( configmap.contains( QStringLiteral( "definedid" ) ) )
310   {
311     tabConfigs->setCurrentIndex( definedTab() );
312     const QString definedid = configmap.value( QStringLiteral( "definedid" ) );
313     setCurrentDefinedConfig( definedid );
314     if ( !definedid.isEmpty() )
315     {
316       if ( !configmap.value( QStringLiteral( "defineddirpath" ) ).isEmpty() )
317       {
318         // this will trigger a reload of dirs and a reselection of any existing defined id
319         leDefinedDirPath->setText( configmap.value( QStringLiteral( "defineddirpath" ) ) );
320       }
321       else
322       {
323         QgsDebugMsg( QStringLiteral( "No custom defined dir path to load OAuth2 config" ) );
324         selectCurrentDefinedConfig();
325       }
326 
327       const QByteArray querypairstxt = configmap.value( QStringLiteral( "querypairs" ) ).toUtf8();
328       if ( !querypairstxt.isNull() && !querypairstxt.isEmpty() )
329       {
330         const QVariantMap querypairsmap =
331           QgsAuthOAuth2Config::variantFromSerialized( querypairstxt, QgsAuthOAuth2Config::JSON, &ok );
332         if ( ok )
333         {
334           populateQueryPairs( querypairsmap );
335         }
336         else
337         {
338           QgsDebugMsg( QStringLiteral( "No query pairs to load OAuth2 config: failed to parse" ) );
339         }
340       }
341       else
342       {
343         QgsDebugMsg( QStringLiteral( "No query pairs to load OAuth2 config: empty text" ) );
344       }
345     }
346     else
347     {
348       QgsDebugMsg( QStringLiteral( "FAILED to load a defined ID for OAuth2 config" ) );
349     }
350   }
351 
352   validateConfig();
353 }
354 
resetConfig()355 void QgsAuthOAuth2Edit::resetConfig()
356 {
357   loadConfig( mConfigMap );
358 }
359 
clearConfig()360 void QgsAuthOAuth2Edit::clearConfig()
361 {
362   // restore defaults to config objs
363   mOAuthConfigCustom->setToDefaults();
364 
365   mDefinedId.clear();
366 
367   clearQueryPairs();
368 
369   // clear any set predefined location
370   leDefinedDirPath->clear();
371 
372   // reload predefined table
373   loadDefinedConfigs();
374 
375   loadFromOAuthConfig( mOAuthConfigCustom.get() );
376 }
377 
loadFromOAuthConfig(const QgsAuthOAuth2Config * config)378 void QgsAuthOAuth2Edit::loadFromOAuthConfig( const QgsAuthOAuth2Config *config )
379 {
380   if ( !config )
381   {
382     return;
383   }
384 
385   // load relative to config type
386   if ( config->configType() == QgsAuthOAuth2Config::Custom )
387   {
388     if ( config->isValid() )
389     {
390       tabConfigs->setCurrentIndex( customTab() );
391     }
392     pteDescription->setPlainText( config->description() );
393     leRequestUrl->setText( config->requestUrl() );
394     leTokenUrl->setText( config->tokenUrl() );
395     leRefreshTokenUrl->setText( config->refreshTokenUrl() );
396     leRedirectUrl->setText( config->redirectUrl() );
397     spnbxRedirectPort->setValue( config->redirectPort() );
398     leClientId->setText( config->clientId() );
399     leClientSecret->setText( config->clientSecret() );
400     leUsername->setText( config->username() );
401     lePassword->setText( config->password() );
402     leScope->setText( config->scope() );
403     leApiKey->setText( config->apiKey() );
404     mTokenHeaderLineEdit->setText( config->customHeader() );
405 
406     // advanced
407     chkbxTokenPersist->setChecked( config->persistToken() );
408     cmbbxAccessMethod->setCurrentIndex( static_cast<int>( config->accessMethod() ) );
409     spnbxRequestTimeout->setValue( config->requestTimeout() );
410 
411     populateQueryPairs( config->queryPairs() );
412 
413     updateGrantFlow( static_cast<int>( config->grantFlow() ) );
414   }
415 
416   validateConfig();
417 }
418 
updateTokenCacheFile(bool curpersist) const419 void QgsAuthOAuth2Edit::updateTokenCacheFile( bool curpersist ) const
420 {
421   // default for unset persistToken in config and edit GUI is false
422   if ( mPrevPersistToken == curpersist )
423   {
424     return;
425   }
426 
427   if ( !parent() )
428   {
429     QgsDebugMsg( QStringLiteral( "Edit widget has no parent" ) );
430     return;
431   }
432 
433   const QString authcfg = parentConfigId();
434   if ( authcfg.isEmpty() )
435   {
436     QgsDebugMsg( QStringLiteral( "Auth config ID empty in ID widget of parent" ) );
437     return;
438   }
439 
440   const QString localcachefile = QgsAuthOAuth2Config::tokenCachePath( authcfg, false );
441 
442   const QString tempcachefile = QgsAuthOAuth2Config::tokenCachePath( authcfg, true );
443 
444   //QgsDebugMsg( QStringLiteral( "localcachefile: %1" ).arg( localcachefile ) );
445   //QgsDebugMsg( QStringLiteral( "tempcachefile: %1" ).arg( tempcachefile ) );
446 
447   if ( curpersist )
448   {
449     // move cache file from temp dir to local
450     if ( QFile::exists( localcachefile ) && !QFile::remove( localcachefile ) )
451     {
452       QgsDebugMsg( QStringLiteral( "FAILED to delete local token cache file: %1" ).arg( localcachefile ) );
453       return;
454     }
455     if ( QFile::exists( tempcachefile ) && !QFile::copy( tempcachefile, localcachefile ) )
456     {
457       QgsDebugMsg( QStringLiteral( "FAILED to copy temp to local token cache file: %1 -> %2" ).arg( tempcachefile, localcachefile ) );
458       return;
459     }
460     if ( QFile::exists( tempcachefile ) && !QFile::remove( tempcachefile ) )
461     {
462       QgsDebugMsg( QStringLiteral( "FAILED to delete temp token cache file after copy: %1" ).arg( tempcachefile ) );
463       return;
464     }
465   }
466   else
467   {
468     // move cache file from local to temp
469     if ( QFile::exists( tempcachefile ) && !QFile::remove( tempcachefile ) )
470     {
471       QgsDebugMsg( QStringLiteral( "FAILED to delete temp token cache file: %1" ).arg( tempcachefile ) );
472       return;
473     }
474     if ( QFile::exists( localcachefile ) && !QFile::copy( localcachefile, tempcachefile ) )
475     {
476       QgsDebugMsg( QStringLiteral( "FAILED to copy local to temp token cache file: %1 -> %2" ).arg( localcachefile, tempcachefile ) );
477       return;
478     }
479     if ( QFile::exists( localcachefile ) && !QFile::remove( localcachefile ) )
480     {
481       QgsDebugMsg( QStringLiteral( "FAILED to delete temp token cache file after copy: %1" ).arg( localcachefile ) );
482       return;
483     }
484   }
485 }
486 
487 
tabIndexChanged(int indx)488 void QgsAuthOAuth2Edit::tabIndexChanged( int indx )
489 {
490   mCurTab = indx;
491   validateConfig();
492 }
493 
494 
populateGrantFlows()495 void QgsAuthOAuth2Edit::populateGrantFlows()
496 {
497   cmbbxGrantFlow->addItem( QgsAuthOAuth2Config::grantFlowString( QgsAuthOAuth2Config::AuthCode ),
498                            static_cast<int>( QgsAuthOAuth2Config::AuthCode ) );
499   cmbbxGrantFlow->addItem( QgsAuthOAuth2Config::grantFlowString( QgsAuthOAuth2Config::Implicit ),
500                            static_cast<int>( QgsAuthOAuth2Config::Implicit ) );
501   cmbbxGrantFlow->addItem( QgsAuthOAuth2Config::grantFlowString( QgsAuthOAuth2Config::ResourceOwner ),
502                            static_cast<int>( QgsAuthOAuth2Config::ResourceOwner ) );
503 }
504 
505 
definedCustomDirChanged(const QString & path)506 void QgsAuthOAuth2Edit::definedCustomDirChanged( const QString &path )
507 {
508   const QFileInfo pinfo( path );
509   const bool ok = pinfo.exists() || pinfo.isDir();
510 
511   leDefinedDirPath->setStyleSheet( ok ? QString() : QgsAuthGuiUtils::redTextStyleSheet() );
512   updatePredefinedLocationsTooltip();
513 
514   if ( ok )
515   {
516     loadDefinedConfigs();
517   }
518 }
519 
520 
softwareStatementJwtPathChanged(const QString & path)521 void QgsAuthOAuth2Edit::softwareStatementJwtPathChanged( const QString &path )
522 {
523   const QFileInfo pinfo( path );
524   const bool ok = pinfo.exists() || pinfo.isFile();
525 
526   leSoftwareStatementJwtPath->setStyleSheet( ok ? QString() : QgsAuthGuiUtils::redTextStyleSheet() );
527 
528   if ( ok )
529   {
530     parseSoftwareStatement( path );
531   }
532 }
533 
534 
setCurrentDefinedConfig(const QString & id)535 void QgsAuthOAuth2Edit::setCurrentDefinedConfig( const QString &id )
536 {
537   mDefinedId = id;
538   QgsDebugMsg( QStringLiteral( "Set defined ID: %1" ).arg( id ) );
539   validateConfig();
540 }
541 
currentDefinedItemChanged(QListWidgetItem * cur,QListWidgetItem * prev)542 void QgsAuthOAuth2Edit::currentDefinedItemChanged( QListWidgetItem *cur, QListWidgetItem *prev )
543 {
544   Q_UNUSED( prev )
545 
546   QgsDebugMsg( QStringLiteral( "Entered" ) );
547 
548   const QString id = cur->data( Qt::UserRole ).toString();
549   if ( !id.isEmpty() )
550   {
551     setCurrentDefinedConfig( id );
552   }
553 }
554 
555 
selectCurrentDefinedConfig()556 void QgsAuthOAuth2Edit::selectCurrentDefinedConfig()
557 {
558   if ( mDefinedId.isEmpty() )
559   {
560     return;
561   }
562 
563   if ( !onDefinedTab() )
564   {
565     tabConfigs->setCurrentIndex( definedTab() );
566   }
567 
568   lstwdgDefinedConfigs->selectionModel()->clearSelection();
569 
570   for ( int i = 0; i < lstwdgDefinedConfigs->count(); ++i )
571   {
572     QListWidgetItem *itm = lstwdgDefinedConfigs->item( i );
573 
574     if ( itm->data( Qt::UserRole ).toString() == mDefinedId )
575     {
576       lstwdgDefinedConfigs->setCurrentItem( itm, QItemSelectionModel::Select );
577       break;
578     }
579   }
580 }
581 
getDefinedCustomDir()582 void QgsAuthOAuth2Edit::getDefinedCustomDir()
583 {
584   const QString extradir = QFileDialog::getExistingDirectory( this, tr( "Select extra directory to parse" ),
585                            QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks );
586   this->raise();
587   this->activateWindow();
588 
589   if ( extradir.isEmpty() )
590   {
591     return;
592   }
593   leDefinedDirPath->setText( extradir );
594 }
595 
getSoftStatementDir()596 void QgsAuthOAuth2Edit::getSoftStatementDir()
597 {
598   const QString softStatementFile = QFileDialog::getOpenFileName( this, tr( "Select software statement file" ),
599                                     QDir::homePath(), tr( "JSON Web Token (*.jwt)" ) );
600   this->raise();
601   this->activateWindow();
602 
603   if ( softStatementFile.isEmpty() )
604   {
605     return;
606   }
607   leSoftwareStatementJwtPath->setText( softStatementFile );
608 }
609 
initConfigObjs()610 void QgsAuthOAuth2Edit::initConfigObjs()
611 {
612   mOAuthConfigCustom = std::make_unique<QgsAuthOAuth2Config>( nullptr );
613   mOAuthConfigCustom->setConfigType( QgsAuthOAuth2Config::Custom );
614   mOAuthConfigCustom->setToDefaults();
615 }
616 
617 
hasTokenCacheFile()618 bool QgsAuthOAuth2Edit::hasTokenCacheFile()
619 {
620   const QString authcfg = parentConfigId();
621   if ( authcfg.isEmpty() )
622   {
623     QgsDebugMsg( QStringLiteral( "Auth config ID empty in ID widget of parent" ) );
624     return false;
625   }
626 
627   return ( QFile::exists( QgsAuthOAuth2Config::tokenCachePath( authcfg, false ) )
628            || QFile::exists( QgsAuthOAuth2Config::tokenCachePath( authcfg, true ) ) );
629 }
630 
631 //slot
removeTokenCacheFile()632 void QgsAuthOAuth2Edit::removeTokenCacheFile()
633 {
634   const QString authcfg = parentConfigId();
635   if ( authcfg.isEmpty() )
636   {
637     QgsDebugMsg( QStringLiteral( "Auth config ID empty in ID widget of parent" ) );
638     return;
639   }
640 
641   const QStringList cachefiles = QStringList()
642                                  << QgsAuthOAuth2Config::tokenCachePath( authcfg, false )
643                                  << QgsAuthOAuth2Config::tokenCachePath( authcfg, true );
644 
645   for ( const QString &cachefile : cachefiles )
646   {
647     if ( QFile::exists( cachefile ) && !QFile::remove( cachefile ) )
648     {
649       QgsDebugMsg( QStringLiteral( "Remove token cache file FAILED for authcfg %1: %2" ).arg( authcfg, cachefile ) );
650     }
651   }
652   btnTokenClear->setEnabled( hasTokenCacheFile() );
653 }
654 
updateDefinedConfigsCache()655 void QgsAuthOAuth2Edit::updateDefinedConfigsCache()
656 {
657   const QString extradir = leDefinedDirPath->text();
658   mDefinedConfigsCache.clear();
659   mDefinedConfigsCache = QgsAuthOAuth2Config::mappedOAuth2ConfigsCache( this, extradir );
660 }
661 
loadDefinedConfigs()662 void QgsAuthOAuth2Edit::loadDefinedConfigs()
663 {
664   whileBlocking( lstwdgDefinedConfigs )->clear();
665   updateDefinedConfigsCache();
666   updatePredefinedLocationsTooltip();
667 
668   QgsStringMap::const_iterator i = mDefinedConfigsCache.constBegin();
669   while ( i != mDefinedConfigsCache.constEnd() )
670   {
671     QgsAuthOAuth2Config *config = new QgsAuthOAuth2Config( this );
672     if ( !config->loadConfigTxt( i.value().toUtf8(), QgsAuthOAuth2Config::JSON ) )
673     {
674       QgsDebugMsg( QStringLiteral( "FAILED to load config for ID: %1" ).arg( i.key() ) );
675       config->deleteLater();
676       continue;
677     }
678 
679     const QString grantflow = QgsAuthOAuth2Config::grantFlowString( config->grantFlow() );
680 
681     const QString name = QStringLiteral( "%1 (%2): %3" )
682                          .arg( config->name(), grantflow, config->description() );
683 
684     const QString tip = tr( "ID: %1\nGrant flow: %2\nDescription: %3" )
685                         .arg( i.key(), grantflow, config->description() );
686 
687     QListWidgetItem *itm = new QListWidgetItem( lstwdgDefinedConfigs );
688     itm->setText( name );
689     itm->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable );
690     itm->setData( Qt::UserRole, QVariant( i.key() ) );
691     itm->setData( Qt::ToolTipRole, QVariant( tip ) );
692     lstwdgDefinedConfigs->addItem( itm );
693 
694     config->deleteLater();
695     ++i;
696   }
697 
698   if ( lstwdgDefinedConfigs->count() == 0 )
699   {
700     QListWidgetItem *itm = new QListWidgetItem( lstwdgDefinedConfigs );
701     itm->setText( tr( "No predefined configurations found on disk" ) );
702     QFont f( itm->font() );
703     f.setItalic( true );
704     itm->setFont( f );
705     itm->setFlags( Qt::NoItemFlags );
706     lstwdgDefinedConfigs->addItem( itm );
707   }
708 
709   selectCurrentDefinedConfig();
710 }
711 
onCustomTab() const712 bool QgsAuthOAuth2Edit::onCustomTab() const
713 {
714   return mCurTab == customTab();
715 }
716 
onDefinedTab() const717 bool QgsAuthOAuth2Edit::onDefinedTab() const
718 {
719   return mCurTab == definedTab();
720 }
721 
onStatementTab() const722 bool QgsAuthOAuth2Edit::onStatementTab() const
723 {
724   return mCurTab == statementTab();
725 }
726 
updateGrantFlow(int indx)727 void QgsAuthOAuth2Edit::updateGrantFlow( int indx )
728 {
729   if ( cmbbxGrantFlow->currentIndex() != indx )
730   {
731     whileBlocking( cmbbxGrantFlow )->setCurrentIndex( indx );
732   }
733 
734   const QgsAuthOAuth2Config::GrantFlow flow =
735     static_cast<QgsAuthOAuth2Config::GrantFlow>( cmbbxGrantFlow->itemData( indx ).toInt() );
736   mOAuthConfigCustom->setGrantFlow( flow );
737 
738   // bool authcode = ( flow == QgsAuthOAuth2Config::AuthCode );
739   const bool implicit = ( flow == QgsAuthOAuth2Config::Implicit );
740   const bool resowner = ( flow == QgsAuthOAuth2Config::ResourceOwner );
741 
742   lblRequestUrl->setVisible( !resowner );
743   leRequestUrl->setVisible( !resowner );
744   if ( resowner )
745     leRequestUrl->setText( QString() );
746 
747   lblRedirectUrl->setVisible( !resowner );
748   frameRedirectUrl->setVisible( !resowner );
749 
750   lblClientSecret->setVisible( !implicit );
751   leClientSecret->setVisible( !implicit );
752   if ( implicit )
753     leClientSecret->setText( QString() );
754 
755   leClientId->setPlaceholderText( resowner ? tr( "Optional" ) : tr( "Required" ) );
756   leClientSecret->setPlaceholderText( resowner ? tr( "Optional" ) : tr( "Required" ) );
757 
758 
759   lblUsername->setVisible( resowner );
760   leUsername->setVisible( resowner );
761   if ( !resowner )
762     leUsername->setText( QString() );
763   lblPassword->setVisible( resowner );
764   lePassword->setVisible( resowner );
765   if ( !resowner )
766     lePassword->setText( QString() );
767 }
768 
769 
exportOAuthConfig()770 void QgsAuthOAuth2Edit::exportOAuthConfig()
771 {
772   if ( !onCustomTab() || !mValid )
773   {
774     return;
775   }
776 
777   QSettings settings;
778   const QString recentdir = settings.value( QStringLiteral( "UI/lastAuthSaveFileDir" ), QDir::homePath() ).toString();
779   const QString configpath = QFileDialog::getSaveFileName(
780                                this, tr( "Save OAuth2 Config File" ), recentdir, QStringLiteral( "OAuth2 config files (*.json)" ) );
781   this->raise();
782   this->activateWindow();
783 
784   if ( configpath.isEmpty() )
785   {
786     return;
787   }
788   settings.setValue( QStringLiteral( "UI/lastAuthSaveFileDir" ), QFileInfo( configpath ).absoluteDir().path() );
789 
790   // give it a kind of random id for re-importing
791   mOAuthConfigCustom->setId( QgsApplication::authManager()->uniqueConfigId() );
792 
793   mOAuthConfigCustom->setQueryPairs( queryPairs() );
794 
795   if ( mParentName && !mParentName->text().isEmpty() )
796   {
797     mOAuthConfigCustom->setName( mParentName->text() );
798   }
799 
800   if ( !QgsAuthOAuth2Config::writeOAuth2Config( configpath, mOAuthConfigCustom.get(),
801        QgsAuthOAuth2Config::JSON, true ) )
802   {
803     QgsDebugMsg( QStringLiteral( "FAILED to export OAuth2 config file" ) );
804   }
805   // clear temp changes
806   mOAuthConfigCustom->setId( QString() );
807   mOAuthConfigCustom->setName( QString() );
808 }
809 
810 
importOAuthConfig()811 void QgsAuthOAuth2Edit::importOAuthConfig()
812 {
813   if ( !onCustomTab() )
814   {
815     return;
816   }
817 
818   const QString configfile =
819     QgsAuthGuiUtils::getOpenFileName( this, tr( "Select OAuth2 Config File" ), QStringLiteral( "OAuth2 config files (*.json)" ) );
820   this->raise();
821   this->activateWindow();
822 
823   const QFileInfo importinfo( configfile );
824   if ( configfile.isEmpty() || !importinfo.exists() )
825   {
826     return;
827   }
828 
829   QByteArray configtxt;
830   QFile cfile( configfile );
831   const bool ret = cfile.open( QIODevice::ReadOnly | QIODevice::Text );
832   if ( ret )
833   {
834     configtxt = cfile.readAll();
835   }
836   else
837   {
838     QgsDebugMsg( QStringLiteral( "FAILED to open config for reading: %1" ).arg( configfile ) );
839     cfile.close();
840     return;
841   }
842   cfile.close();
843 
844   if ( configtxt.isEmpty() )
845   {
846     QgsDebugMsg( QStringLiteral( "EMPTY read of config: %1" ).arg( configfile ) );
847     return;
848   }
849 
850   QgsStringMap configmap;
851   configmap.insert( QStringLiteral( "oauth2config" ), QString( configtxt ) );
852   loadConfig( configmap );
853 }
854 
855 
descriptionChanged()856 void QgsAuthOAuth2Edit::descriptionChanged()
857 {
858   mOAuthConfigCustom->setDescription( pteDescription->toPlainText() );
859 }
860 
861 
populateAccessMethods()862 void QgsAuthOAuth2Edit::populateAccessMethods()
863 {
864   cmbbxAccessMethod->addItem( QgsAuthOAuth2Config::accessMethodString( QgsAuthOAuth2Config::Header ),
865                               static_cast<int>( QgsAuthOAuth2Config::Header ) );
866   cmbbxAccessMethod->addItem( QgsAuthOAuth2Config::accessMethodString( QgsAuthOAuth2Config::Form ),
867                               static_cast<int>( QgsAuthOAuth2Config::Form ) );
868   cmbbxAccessMethod->addItem( QgsAuthOAuth2Config::accessMethodString( QgsAuthOAuth2Config::Query ),
869                               static_cast<int>( QgsAuthOAuth2Config::Query ) );
870 }
871 
872 
updateConfigAccessMethod(int indx)873 void QgsAuthOAuth2Edit::updateConfigAccessMethod( int indx )
874 {
875   mOAuthConfigCustom->setAccessMethod( static_cast<QgsAuthOAuth2Config::AccessMethod>( indx ) );
876   switch ( static_cast<QgsAuthOAuth2Config::AccessMethod>( indx ) )
877   {
878     case QgsAuthOAuth2Config::Header:
879       mTokenHeaderLineEdit->setVisible( true );
880       mTokenHeaderLabel->setVisible( true );
881       break;
882     case QgsAuthOAuth2Config::Form:
883     case QgsAuthOAuth2Config::Query:
884       mTokenHeaderLineEdit->setVisible( false );
885       mTokenHeaderLabel->setVisible( false );
886       break;
887   }
888 }
889 
addQueryPairRow(const QString & key,const QString & val)890 void QgsAuthOAuth2Edit::addQueryPairRow( const QString &key, const QString &val )
891 {
892   const int rowCnt = tblwdgQueryPairs->rowCount();
893   tblwdgQueryPairs->insertRow( rowCnt );
894 
895   const Qt::ItemFlags itmFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable
896                                  | Qt::ItemIsEditable | Qt::ItemIsDropEnabled;
897 
898   QTableWidgetItem *keyItm = new QTableWidgetItem( key );
899   keyItm->setFlags( itmFlags );
900   tblwdgQueryPairs->setItem( rowCnt, 0, keyItm );
901 
902   QTableWidgetItem *valItm = new QTableWidgetItem( val );
903   keyItm->setFlags( itmFlags );
904   tblwdgQueryPairs->setItem( rowCnt, 1, valItm );
905 }
906 
907 
populateQueryPairs(const QVariantMap & querypairs,bool append)908 void QgsAuthOAuth2Edit::populateQueryPairs( const QVariantMap &querypairs, bool append )
909 {
910   if ( !append )
911   {
912     clearQueryPairs();
913   }
914 
915   QVariantMap::const_iterator i = querypairs.constBegin();
916   while ( i != querypairs.constEnd() )
917   {
918     addQueryPairRow( i.key(), i.value().toString() );
919     ++i;
920   }
921 }
922 
923 
queryTableSelectionChanged()924 void QgsAuthOAuth2Edit::queryTableSelectionChanged()
925 {
926   const bool hassel = tblwdgQueryPairs->selectedItems().count() > 0;
927   btnRemoveQueryPair->setEnabled( hassel );
928 }
929 
updateConfigQueryPairs()930 void QgsAuthOAuth2Edit::updateConfigQueryPairs()
931 {
932   mOAuthConfigCustom->setQueryPairs( queryPairs() );
933 }
934 
queryPairs() const935 QVariantMap QgsAuthOAuth2Edit::queryPairs() const
936 {
937   QVariantMap querypairs;
938   for ( int i = 0; i < tblwdgQueryPairs->rowCount(); ++i )
939   {
940     if ( tblwdgQueryPairs->item( i, 0 )->text().isEmpty() )
941     {
942       continue;
943     }
944     querypairs.insert( tblwdgQueryPairs->item( i, 0 )->text(),
945                        QVariant( tblwdgQueryPairs->item( i, 1 )->text() ) );
946   }
947   return querypairs;
948 }
949 
950 
addQueryPair()951 void QgsAuthOAuth2Edit::addQueryPair()
952 {
953   addQueryPairRow( QString(), QString() );
954   tblwdgQueryPairs->setFocus();
955   tblwdgQueryPairs->setCurrentCell( tblwdgQueryPairs->rowCount() - 1, 0 );
956   tblwdgQueryPairs->edit( tblwdgQueryPairs->currentIndex() );
957 }
958 
959 
removeQueryPair()960 void QgsAuthOAuth2Edit::removeQueryPair()
961 {
962   tblwdgQueryPairs->removeRow( tblwdgQueryPairs->currentRow() );
963 }
964 
965 
clearQueryPairs()966 void QgsAuthOAuth2Edit::clearQueryPairs()
967 {
968   for ( int i = tblwdgQueryPairs->rowCount(); i > 0 ; --i )
969   {
970     tblwdgQueryPairs->removeRow( i - 1 );
971   }
972 }
973 
parseSoftwareStatement(const QString & path)974 void QgsAuthOAuth2Edit::parseSoftwareStatement( const QString &path )
975 {
976   QFile file( path );
977   QByteArray softwareStatementBase64;
978   if ( file.open( QIODevice::ReadOnly | QIODevice::Text ) )
979   {
980     softwareStatementBase64 = file.readAll();
981   }
982   if ( softwareStatementBase64.isEmpty() )
983   {
984     QgsDebugMsg( QStringLiteral( "Error software statement is empty: %1" ).arg( path ) );
985     file.close();
986     return;
987   }
988   mRegistrationEndpoint = QString();
989   file.close();
990   mSoftwareStatement.insert( QStringLiteral( "software_statement" ), softwareStatementBase64 );
991   QList<QByteArray> payloadParts( softwareStatementBase64.split( '.' ) );
992   if ( payloadParts.count() < 2 )
993   {
994     QgsDebugMsg( QStringLiteral( "Error parsing JSON: base64 decode returned less than 2 parts" ) );
995     return;
996   }
997   const QByteArray payload = payloadParts[1];
998   QByteArray decoded = QByteArray::fromBase64( payload/*, QByteArray::Base64UrlEncoding*/ );
999   QByteArray errStr;
1000   bool res = false;
1001   const QMap<QString, QVariant> jsonData = QJsonWrapper::parseJson( decoded, &res, &errStr ).toMap();
1002   if ( !res )
1003   {
1004     QgsDebugMsg( QStringLiteral( "Error parsing JSON: %1" ).arg( QString( errStr ) ) );
1005     return;
1006   }
1007   if ( jsonData.contains( QStringLiteral( "grant_types" ) ) && jsonData.contains( QStringLiteral( "redirect_uris" ) ) )
1008   {
1009     const QStringList grantTypes( jsonData[QStringLiteral( "grant_types" ) ].toStringList() );
1010     if ( !grantTypes.isEmpty( ) )
1011     {
1012       const QString grantType = grantTypes[0];
1013       if ( grantType == QLatin1String( "authorization_code" ) )
1014       {
1015         updateGrantFlow( static_cast<int>( QgsAuthOAuth2Config::AuthCode ) );
1016       }
1017       else
1018       {
1019         updateGrantFlow( static_cast<int>( QgsAuthOAuth2Config::ResourceOwner ) );
1020       }
1021     }
1022     //Set redirect_uri
1023     const QStringList  redirectUris( jsonData[QStringLiteral( "redirect_uris" ) ].toStringList() );
1024     if ( !redirectUris.isEmpty( ) )
1025     {
1026       const QString redirectUri = redirectUris[0];
1027       leRedirectUrl->setText( redirectUri );
1028     }
1029   }
1030   else
1031   {
1032     QgsDebugMsgLevel( QStringLiteral( "Error software statement is invalid: %1" ).arg( path ), 4 );
1033     return;
1034   }
1035   if ( jsonData.contains( QStringLiteral( "registration_endpoint" ) ) )
1036   {
1037     mRegistrationEndpoint = jsonData[QStringLiteral( "registration_endpoint" )].toString();
1038     leSoftwareStatementConfigUrl->setText( mRegistrationEndpoint );
1039   }
1040   QgsDebugMsgLevel( QStringLiteral( "JSON: %1" ).arg( QString::fromLocal8Bit( decoded.data() ) ), 4 );
1041 }
1042 
configReplyFinished()1043 void QgsAuthOAuth2Edit::configReplyFinished()
1044 {
1045   qDebug() << "QgsAuthOAuth2Edit::onConfigReplyFinished";
1046   QNetworkReply *configReply = qobject_cast<QNetworkReply *>( sender() );
1047   if ( configReply->error() == QNetworkReply::NoError )
1048   {
1049     const QByteArray replyData = configReply->readAll();
1050     QByteArray errStr;
1051     bool res = false;
1052     const QVariantMap config = QJsonWrapper::parseJson( replyData, &res, &errStr ).toMap();
1053 
1054     if ( !res )
1055     {
1056       QgsDebugMsg( QStringLiteral( "Error parsing JSON: %1" ).arg( QString( errStr ) ) );
1057       return;
1058     }
1059     // I haven't found any docs about the content of this confg JSON file
1060     // I assume that registration_endpoint is all that it MUST contain.
1061     // But we also MAY have other optional information here
1062     if ( config.contains( QStringLiteral( "registration_endpoint" ) ) )
1063     {
1064       if ( config.contains( QStringLiteral( "authorization_endpoint" ) ) )
1065         leRequestUrl->setText( config.value( QStringLiteral( "authorization_endpoint" ) ).toString() );
1066       if ( config.contains( QStringLiteral( "token_endpoint" ) ) )
1067         leTokenUrl->setText( config.value( QStringLiteral( "token_endpoint" ) ).toString() );
1068 
1069       registerSoftStatement( config.value( QStringLiteral( "registration_endpoint" ) ).toString() );
1070     }
1071     else
1072     {
1073       const QString errorMsg = tr( "Downloading configuration failed with error: %1" ).arg( configReply->errorString() );
1074       QgsMessageLog::logMessage( errorMsg, QStringLiteral( "OAuth2" ), Qgis::MessageLevel::Critical );
1075     }
1076   }
1077   mDownloading = false;
1078   configReply->deleteLater();
1079 }
1080 
registerReplyFinished()1081 void QgsAuthOAuth2Edit::registerReplyFinished()
1082 {
1083   //JSV todo
1084   //better error handling
1085   qDebug() << "QgsAuthOAuth2Edit::onRegisterReplyFinished";
1086   QNetworkReply *registerReply = qobject_cast<QNetworkReply *>( sender() );
1087   if ( registerReply->error() == QNetworkReply::NoError )
1088   {
1089     const QByteArray replyData = registerReply->readAll();
1090     QByteArray errStr;
1091     bool res = false;
1092     const QVariantMap clientInfo = QJsonWrapper::parseJson( replyData, &res, &errStr ).toMap();
1093 
1094     // According to RFC 7591 sec. 3.2.1.  Client Information Response the only
1095     // required field is client_id
1096     leClientId->setText( clientInfo.value( QStringLiteral( "client_id" ) ).toString() );
1097     if ( clientInfo.contains( QStringLiteral( "client_secret" ) ) )
1098       leClientSecret->setText( clientInfo.value( QStringLiteral( "client_secret" ) ).toString() );
1099     if ( clientInfo.contains( QStringLiteral( "authorization_endpoint" ) ) )
1100       leRequestUrl->setText( clientInfo.value( QStringLiteral( "authorization_endpoint" ) ).toString() );
1101     if ( clientInfo.contains( QStringLiteral( "token_endpoint" ) ) )
1102       leTokenUrl->setText( clientInfo.value( QStringLiteral( "token_endpoint" ) ).toString() );
1103     if ( clientInfo.contains( QStringLiteral( "scopes" ) ) )
1104       leScope->setText( clientInfo.value( QStringLiteral( "scopes" ) ).toString() );
1105 
1106     tabConfigs->setCurrentIndex( 0 );
1107   }
1108   else
1109   {
1110     const QString errorMsg = QStringLiteral( "Client registration failed with error: %1" ).arg( registerReply->errorString() );
1111     QgsMessageLog::logMessage( errorMsg, QStringLiteral( "OAuth2" ), Qgis::MessageLevel::Critical );
1112   }
1113   mDownloading = false;
1114   registerReply->deleteLater();
1115 }
1116 
networkError(QNetworkReply::NetworkError error)1117 void QgsAuthOAuth2Edit::networkError( QNetworkReply::NetworkError error )
1118 {
1119   QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
1120   qWarning() << "QgsAuthOAuth2Edit::onNetworkError: " << error << ": " << reply->errorString();
1121   const QString errorMsg = QStringLiteral( "Network error: %1" ).arg( reply->errorString() );
1122   QgsMessageLog::logMessage( errorMsg, QStringLiteral( "OAuth2" ), Qgis::MessageLevel::Critical );
1123   qDebug() << "QgsAuthOAuth2Edit::onNetworkError: " << reply->readAll();
1124 }
1125 
1126 
registerSoftStatement(const QString & registrationUrl)1127 void QgsAuthOAuth2Edit::registerSoftStatement( const QString &registrationUrl )
1128 {
1129   const QUrl regUrl( registrationUrl );
1130   if ( !regUrl.isValid() )
1131   {
1132     qWarning() << "Registration url is not valid";
1133     return;
1134   }
1135   QByteArray errStr;
1136   bool res = false;
1137   const QByteArray json = QJsonWrapper::toJson( QVariant( mSoftwareStatement ), &res, &errStr );
1138   QNetworkRequest registerRequest( regUrl );
1139   QgsSetRequestInitiatorClass( registerRequest, QStringLiteral( "QgsAuthOAuth2Edit" ) );
1140   registerRequest.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/json" ) );
1141   QNetworkReply *registerReply;
1142   // For testability: use GET if protocol is file://
1143   if ( regUrl.scheme() == QLatin1String( "file" ) )
1144     registerReply = QgsNetworkAccessManager::instance()->get( registerRequest );
1145   else
1146     registerReply = QgsNetworkAccessManager::instance()->post( registerRequest, json );
1147   mDownloading = true;
1148   connect( registerReply, &QNetworkReply::finished, this, &QgsAuthOAuth2Edit::registerReplyFinished, Qt::QueuedConnection );
1149 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
1150   connect( registerReply, qOverload<QNetworkReply::NetworkError>( &QNetworkReply::error ), this, &QgsAuthOAuth2Edit::networkError, Qt::QueuedConnection );
1151 #else
1152   connect( registerReply, &QNetworkReply::errorOccurred, this, &QgsAuthOAuth2Edit::networkError, Qt::QueuedConnection );
1153 #endif
1154 }
1155 
getSoftwareStatementConfig()1156 void QgsAuthOAuth2Edit::getSoftwareStatementConfig()
1157 {
1158   if ( !mRegistrationEndpoint.isEmpty() )
1159   {
1160     registerSoftStatement( mRegistrationEndpoint );
1161   }
1162   else
1163   {
1164     const QString config = leSoftwareStatementConfigUrl->text();
1165     const QUrl configUrl( config );
1166     QNetworkRequest configRequest( configUrl );
1167     QgsSetRequestInitiatorClass( configRequest, QStringLiteral( "QgsAuthOAuth2Edit" ) );
1168     QNetworkReply *configReply = QgsNetworkAccessManager::instance()->get( configRequest );
1169     mDownloading = true;
1170     connect( configReply, &QNetworkReply::finished, this, &QgsAuthOAuth2Edit::configReplyFinished, Qt::QueuedConnection );
1171 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
1172     connect( configReply, qOverload<QNetworkReply::NetworkError>( &QNetworkReply::error ), this, &QgsAuthOAuth2Edit::networkError, Qt::QueuedConnection );
1173 #else
1174     connect( configReply, &QNetworkReply::errorOccurred, this, &QgsAuthOAuth2Edit::networkError, Qt::QueuedConnection );
1175 #endif
1176   }
1177 }
1178 
updatePredefinedLocationsTooltip()1179 void QgsAuthOAuth2Edit::updatePredefinedLocationsTooltip()
1180 {
1181   const QStringList dirs = QgsAuthOAuth2Config::configLocations( leDefinedDirPath->text() );
1182   QString locationList;
1183   QString locationListHtml;
1184   for ( const QString &dir : dirs )
1185   {
1186     if ( !locationList.isEmpty() )
1187       locationList += '\n';
1188     if ( locationListHtml.isEmpty() )
1189       locationListHtml = QStringLiteral( "<ul>" );
1190     locationList += QStringLiteral( "• %1" ).arg( dir );
1191     locationListHtml += QStringLiteral( "<li><a href=\"%1\">%2</a></li>" ).arg( QUrl::fromLocalFile( dir ).toString(), dir );
1192   }
1193   if ( !locationListHtml.isEmpty() )
1194     locationListHtml += QLatin1String( "</ul>" );
1195 
1196   const QString tip = QStringLiteral( "<p>" ) + tr( "Defined configurations are JSON-formatted files, with a single configuration per file. "
1197                       "This allows configurations to be swapped out via filesystem tools without affecting user "
1198                       "configurations. It is recommended to use the Configure tab’s export function, then edit the "
1199                       "resulting file. See QGIS documentation for further details." ) + QStringLiteral( "</p><p>" ) +
1200                       tr( "Configurations files can be placed in the directories:" ) + QStringLiteral( "</p>" ) + locationListHtml;
1201   pteDefinedDesc->setHtml( tip );
1202 
1203   lstwdgDefinedConfigs->setToolTip( tr( "Configuration files can be placed in the directories:\n\n%1" ).arg( locationList ) );
1204 }
1205 
1206