diff --git a/providers/ghprovider/ghdialog.cpp b/providers/ghprovider/ghdialog.cpp index 5f1606410a..7f2a84b9f2 100644 --- a/providers/ghprovider/ghdialog.cpp +++ b/providers/ghprovider/ghdialog.cpp @@ -1,167 +1,169 @@ /* This file is part of KDevelop * * Copyright (C) 2012-2013 Miquel Sabaté * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include #include #include #include #include #define VALID_ACCOUNT "You're logged in as %1. You can check the " \ "authorization for this application and others " \ "here." #define INVALID_ACCOUNT "You haven't authorized KDevelop to use your Github " \ "account. If you authorize KDevelop, you will be able to fetch your " \ "public/private repositories and the repositories from your organizations." namespace gh { Dialog::Dialog(QWidget *parent, Account *account) : QDialog(parent) , m_account(account) { auto mainWidget = new QWidget(this); auto mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); auto buttonBox = new QDialogButtonBox(); if (m_account->validAccount()) { QString str = QString(VALID_ACCOUNT).arg(m_account->name()); m_text = new QLabel(i18n(str.toUtf8()), this); auto logOutButton = new QPushButton; logOutButton->setText(i18n("Log out")); logOutButton->setIcon(QIcon::fromTheme("dialog-cancel")); buttonBox->addButton(logOutButton, QDialogButtonBox::ActionRole); connect(logOutButton, &QPushButton::clicked, this, &Dialog::revokeAccess); auto forceSyncButton = new QPushButton; forceSyncButton->setText(i18n("Force Sync")); forceSyncButton->setIcon(QIcon::fromTheme("view-refresh")); buttonBox->addButton(forceSyncButton, QDialogButtonBox::ActionRole); connect(forceSyncButton, &QPushButton::clicked, this, &Dialog::syncUser); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); } else { m_text = new QLabel(i18n(INVALID_ACCOUNT), this); buttonBox->addButton(QDialogButtonBox::Cancel); auto authorizeButton = new QPushButton; buttonBox->addButton(authorizeButton, QDialogButtonBox::ActionRole); authorizeButton->setText(i18n("Authorize")); authorizeButton->setIcon(QIcon::fromTheme("dialog-ok")); connect(authorizeButton, &QPushButton::clicked, this, &Dialog::authorizeClicked); } m_text->setWordWrap(true); m_text->setOpenExternalLinks(true); setMinimumWidth(350); mainLayout->addWidget(m_text); mainLayout->addWidget(buttonBox); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); setWindowTitle(i18n("Github Account")); } void Dialog::authorizeClicked() { KPasswordDialog dlg(this, KPasswordDialog::ShowUsernameLine); dlg.setPrompt(i18n("Enter a login and a password")); if(!dlg.exec()) return; m_text->setAlignment(Qt::AlignCenter); m_text->setText(i18n("Waiting for response")); m_account->setName(dlg.username()); Resource *rs = m_account->resource(); rs->authenticate(dlg.username(), dlg.password()); connect(rs, &Resource::authenticated, this, &Dialog::authorizeResponse); } -void Dialog::authorizeResponse(const QByteArray &id, const QByteArray &token) +void Dialog::authorizeResponse(const QByteArray &id, const QByteArray &token, const QString &tokenName) { + Q_UNUSED(tokenName); + Resource *rs = m_account->resource(); disconnect(rs, &Resource::authenticated, this, &Dialog::authorizeResponse); if (id.isEmpty()) { m_text->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_text->setText(i18n(INVALID_ACCOUNT)); m_account->setName(QString()); KMessageBox::sorry(this, i18n("Authentication failed! Please, " "try again")); return; } m_account->saveToken(id, token); syncUser(); } void Dialog::syncUser() { Resource *rs = m_account->resource(); connect(rs, &Resource::orgsUpdated, this, &Dialog::updateOrgs); m_text->setAlignment(Qt::AlignCenter); m_text->setText(i18n("Waiting for response")); rs->getOrgs(m_account->token()); } void Dialog::updateOrgs(const QStringList orgs) { Resource *rs = m_account->resource(); disconnect(rs, &Resource::orgsUpdated, this, &Dialog::updateOrgs); if (!orgs.isEmpty()) m_account->setOrgs(orgs); emit shouldUpdate(); close(); } void Dialog::revokeAccess() { KPasswordDialog dlg(this); dlg.setPrompt(i18n("Please, write your password here.")); if(!dlg.exec()) return; m_account->invalidate(dlg.password()); emit shouldUpdate(); close(); } } // End of namespace gh diff --git a/providers/ghprovider/ghdialog.h b/providers/ghprovider/ghdialog.h index 0c3aa1bbc9..ce393c2407 100644 --- a/providers/ghprovider/ghdialog.h +++ b/providers/ghprovider/ghdialog.h @@ -1,99 +1,99 @@ /* This file is part of KDevelop * * Copyright (C) 2012-2013 Miquel Sabaté * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef GH_DIALOG_H #define GH_DIALOG_H #include class QLabel; namespace gh { class Account; /** * @class Dialog * * The Dialog class handles the configuration dialog to be shown so the * user can setup its Github account. */ class Dialog : public QDialog { Q_OBJECT public: /** * Constructor. * * @param parent The QWidget this Dialog is parented to. * @param account The user's account. */ explicit Dialog(QWidget *parent, Account *account); signals: /** * This signal is emitted whenever the dialog has successfully performed * an action that may imply some changes on the UI of the plugin. */ void shouldUpdate(); private slots: /** * The "Authorize" button has been clicked. a KPasswordDialog will be * shown and a request will be sent to Github to authenticate the given * user. */ void authorizeClicked(); /** * Handle a response from Github on the authorization process. If the * given id is empty, it means that something went wrong. Otherwise, when * successful, it will also call the syncUser method. * * @param id The id of the authorization. * @param token The authorization token. */ - void authorizeResponse(const QByteArray &id, const QByteArray &token); + void authorizeResponse(const QByteArray &id, const QByteArray &token, const QString &tokenName); /// Sync the user's groups. void syncUser(); /** * Update the organization list of the current user. * * @param orgs A list of organizations for the current user. */ void updateOrgs(const QStringList orgs); /// Revoke the access of this application. void revokeAccess(); private: Account *m_account; QString m_name; QLabel *m_text; }; } // End of namespace gh #endif /* GH_DIALOG_H */ diff --git a/providers/ghprovider/ghresource.cpp b/providers/ghprovider/ghresource.cpp index 71456ba398..75ac4e70f4 100644 --- a/providers/ghprovider/ghresource.cpp +++ b/providers/ghprovider/ghresource.cpp @@ -1,193 +1,205 @@ /* This file is part of KDevelop * * Copyright (C) 2012-2013 Miquel Sabaté * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include +#include +#include #include "debug.h" #include #include namespace gh { /// Base url for the Github API v3. const static QUrl baseUrl("https://api.github.com"); Resource::Resource(QObject *parent, ProviderModel *model) : QObject(parent), m_model(model) { /* There's nothing to do here */ } void Resource::searchRepos(const QString &uri, const QString &token) { KIO::TransferJob *job = getTransferJob(uri, token); connect(job, &KIO::TransferJob::data, this, &Resource::slotRepos); } void Resource::getOrgs(const QString &token) { KIO::TransferJob *job = getTransferJob("/user/orgs", token); connect(job, &KIO::TransferJob::data, this, &Resource::slotOrgs); } void Resource::authenticate(const QString &name, const QString &password) { QUrl url = baseUrl; url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + '/' + "/authorizations"); - QByteArray data = "{ \"scopes\": [\"repo\"], \"note\": \"KDevelop Github Provider\" }"; + + // generate a unique token, see bug 372144 + const QString tokenName = "KDevelop Github Provider : " + + QHostInfo::localHostName() + " - " + + QDateTime::currentDateTimeUtc().toString(); + const QByteArray data = "{ \"scopes\": [\"repo\"], \"note\": \"" + tokenName.toUtf8() + "\" }"; + KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo); + job->setProperty("requestedTokenName", tokenName); job->addMetaData("customHTTPHeader", "Authorization: Basic " + QString (name + ':' + password).toUtf8().toBase64()); connect(job, &KIO::StoredTransferJob::result, this, &Resource::slotAuthenticate); job->start(); } void Resource::revokeAccess(const QString &id, const QString &name, const QString &password) { QUrl url = baseUrl; url.setPath(url.path() + "/authorizations/" + id); KIO::TransferJob *job = KIO::http_delete(url, KIO::HideProgressInfo); job->addMetaData("customHTTPHeader", "Authorization: Basic " + QString (name + ':' + password).toUtf8().toBase64()); /* And we don't care if it's successful ;) */ job->start(); } KIO::TransferJob * Resource::getTransferJob(const QString &uri, const QString &token) const { QUrl url = baseUrl; url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + '/' + uri); KIO::TransferJob *job = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); if (!token.isEmpty()) job->addMetaData("customHTTPHeader", "Authorization: token " + token); return job; } void Resource::retrieveRepos(const QByteArray &data) { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if (error.error == 0) { QVariantList map = doc.toVariant().toList(); m_model->clear(); foreach (const QVariant &it, map) { const QVariantMap &map = it.toMap(); Response res; res.name = map.value("name").toString(); res.url = map.value("clone_url").toUrl(); if (map.value("fork").toBool()) res.kind = Fork; else if (map.value("private").toBool()) res.kind = Private; else res.kind = Public; ProviderItem *item = new ProviderItem(res); m_model->appendRow(item); } } emit reposUpdated(); } void Resource::retrieveOrgs(const QByteArray &data) { QStringList res; QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if (error.error == 0) { QVariantList json = doc.toVariant().toList(); foreach (QVariant it, json) { QVariantMap map = it.toMap(); res << map.value("login").toString(); } } emit orgsUpdated(res); } void Resource::slotAuthenticate(KJob *job) { + const QString tokenName = job->property("requestedTokenName").toString(); + Q_ASSERT(!tokenName.isEmpty()); + if (job->error()) { - emit authenticated("", ""); + emit authenticated("", "", tokenName); return; } QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(qobject_cast(job)->data(), &error); qCDebug(GHPROVIDER) << "Response:" << doc; if (error.error == 0) { QVariantMap map = doc.toVariant().toMap(); emit authenticated(map.value("id").toByteArray(), - map.value("token").toByteArray()); + map.value("token").toByteArray(), tokenName); } else - emit authenticated("", ""); + emit authenticated("", "", tokenName); } void Resource::slotRepos(KIO::Job *job, const QByteArray &data) { if (!job) { qWarning() << "NULL job returned!"; return; } if (job->error()) { qWarning() << "Job error: " << job->errorString(); return; } m_temp.append(data); if (data.isEmpty()) { retrieveRepos(m_temp); m_temp = ""; } } void Resource::slotOrgs(KIO::Job *job, const QByteArray &data) { QList res; if (!job) { qWarning() << "NULL job returned!"; emit orgsUpdated(res); return; } if (job->error()) { qWarning() << "Job error: " << job->errorString(); emit orgsUpdated(res); return; } m_orgTemp.append(data); if (data.isEmpty()) { retrieveOrgs(m_orgTemp); m_orgTemp = ""; } } } // End of namespace gh diff --git a/providers/ghprovider/ghresource.h b/providers/ghprovider/ghresource.h index 9bfe17759f..72cfa607d2 100644 --- a/providers/ghprovider/ghresource.h +++ b/providers/ghprovider/ghresource.h @@ -1,178 +1,181 @@ /* This file is part of KDevelop * * Copyright (C) 2012-2013 Miquel Sabaté * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef GH_RESOURCE_H #define GH_RESOURCE_H #include class KJob; namespace KIO { class Job; class TransferJob; } namespace gh { class ProviderModel; /** * @class Resource * * This class provides methods that extract information from the given * Github's JSON responses. */ class Resource : public QObject { Q_OBJECT public: /** * Constructor. * * @param parent The QObject this Resource is parented to. * @param model The model to be used in the other methods. */ explicit Resource(QObject *parent, ProviderModel *model); /** * Search repos by calling the Github API. When successful, it will re-fill * the model provided in the constructor. Otherwise, if something went * wrong, it will do nothing. * * @param uri A string containing the URI to be called to retrieve * the repos. Therefore, this parameter also determines whether the repos * belong to a user or a organization. * @param token The authorization token. */ void searchRepos(const QString &uri, const QString &token); /** * Get public and private organizations for the currently authenticated * user and save it to its local account. * * @param token The authorization token. */ void getOrgs(const QString &token); /** * Authenticate the current user with the given name and password. * This method will eventually emit the authenticated signal. * * @param name The username of the current user. * @param password The password used to login the current user. */ void authenticate(const QString &name, const QString &password); /** * Revoke an access to the Github API (a.k.a. log out the current user). * * @param id The id of the currently valid authorization. * @param name The name of the current user. * @param password The Github API requires the password again to perform * this action. */ void revokeAccess(const QString &id, const QString &name, const QString &password); private: /** * Get a TransferJob for the given URI and authorization token. * * @param uri The URI to be requested. * @param token The authorization token to be set on the headers. */ KIO::TransferJob * getTransferJob(const QString &uri, const QString &token) const; /** * Retrieve the info of the name and the URL of each repo and append * it in the model that was provided in the constructor. * * @param data A Github response in JSON format. */ void retrieveRepos(const QByteArray &data); /** * Retrieve the organizations for the current user from the given * JSON data. It will emit the orgsUpdated signal. * * @param data A Github response in JSON format. */ void retrieveOrgs(const QByteArray &data); signals: /** * This signal will eventually be sent when the authentication process * has finished. An empty id means that the authentication wasn't * successful. * * @param id The id of the authorization. Empty if something went wrong. - * @param token The authorization token. Empty if something went wrong. + * @param token The authorization token (e.g. "14bf8e87e2ec5fe30f8a6755bda63b5bc4d02e22"). + * Empty if something went wrong. + * @param tokenName The authorization token (e.g. "KDevelop Github Provider : machinename - Thu Nov 17 23:18:03 2016 GMT"). + * Empty if something went wrong. */ - void authenticated(const QByteArray &id, const QByteArray &token); + void authenticated(const QByteArray &id, const QByteArray &token, const QString &tokenName); /** * This signal is emitted when the model containing repos has * been updated. */ void reposUpdated(); /** * This signal is fired when the list of organizations for the current * user has been updated. * * @param orgs A list of the names of the organizations. */ void orgsUpdated(const QStringList &orgs); private slots: /** * Handle the response of the Github authentication process. * * @param job The KJob responsible from the authentication. */ void slotAuthenticate(KJob *job); /** * Handle the response of retrieving repos from Github. * * @param job The job returned after the HTTP request has finished. * @param data The data returned by the HTTP response. */ void slotRepos(KIO::Job *job, const QByteArray &data); /** * Handle the response of retrieving orgs for the currently * authenticated user. * * @param job The job returned after the HTTP request has finished. * @param data The data returned by the HTTP response. */ void slotOrgs(KIO::Job *job, const QByteArray &data); private: ProviderModel *m_model; QByteArray m_temp, m_orgTemp; }; } // End of namespace gh #endif // GH_RESOURCE_H