kicad/common/dialogs/git/dialog_git_repository.cpp

467 lines
14 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors.
*
* 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 3
* 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, you may find one here:
* http://www.gnu.org/licenses/gpl-3.0.html
* or you may search the http://www.gnu.org website for the version 3 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "dialog_git_repository.h"
#include <confirm.h>
#include <git2.h>
#include <gestfich.h>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <wx/clipbrd.h>
#include <wx/msgdlg.h>
#include <wx/regex.h>
#include <wx/stdpaths.h>
DIALOG_GIT_REPOSITORY::DIALOG_GIT_REPOSITORY( wxWindow* aParent, git_repository* aRepository,
wxString aURL ) :
DIALOG_GIT_REPOSITORY_BASE( aParent ),
m_repository( aRepository ),
m_prevFile( wxEmptyString ),
m_tested( 0 ),
m_failedTest( false ),
m_testError( wxEmptyString ),
m_tempRepo( false )
{
m_txtURL->SetFocus();
if( !m_repository )
{
// Make a temporary repository to test the connection
m_tempRepo = true;
m_tempPath = wxFileName::CreateTempFileName( "kicadtestrepo" );
git_repository_init_options options;
git_repository_init_init_options( &options, GIT_REPOSITORY_INIT_OPTIONS_VERSION );
options.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_NO_REINIT;
git_repository_init_ext( &m_repository, m_tempPath.ToStdString().c_str(), &options );
}
if( !aURL.empty() )
m_txtURL->SetValue( aURL );
else
extractClipboardData();
if( !m_txtURL->GetValue().IsEmpty() )
updateURLData();
SetupStandardButtons();
updateAuthControls();
Layout();
}
DIALOG_GIT_REPOSITORY::~DIALOG_GIT_REPOSITORY()
{
if( m_tempRepo )
{
git_repository_free( m_repository );
RmDirRecursive( m_tempPath );
}
}
bool DIALOG_GIT_REPOSITORY::extractClipboardData()
{
if( wxTheClipboard->Open() && wxTheClipboard->IsSupported( wxDF_TEXT ) )
{
wxString clipboardText;
wxTextDataObject textData;
if( wxTheClipboard->GetData( textData ) && !( clipboardText = textData.GetText() ).empty() )
{
if( std::get<0>( isValidHTTPS( clipboardText ) )
|| std::get<0>( isValidSSH( clipboardText ) ) )
{
m_txtURL->SetValue( clipboardText );
}
}
wxTheClipboard->Close();
}
return false;
}
void DIALOG_GIT_REPOSITORY::setDefaultSSHKey()
{
wxFileName sshKey;
sshKey.SetPath( wxGetUserHome() );
wxString retval;
sshKey.AppendDir( ".ssh" );
sshKey.SetFullName( "id_rsa" );
if( sshKey.FileExists() )
{
retval = sshKey.GetFullPath();
}
else if( sshKey.SetFullName( "id_dsa" ); sshKey.FileExists() )
{
retval = sshKey.GetFullPath();
}
else if( sshKey.SetFullName( "id_ecdsa" ); sshKey.FileExists() )
{
retval = sshKey.GetFullPath();
}
if( !retval.empty() )
{
m_fpSSHKey->SetFileName( retval );
wxFileDirPickerEvent evt;
evt.SetPath( retval );
OnFileUpdated( evt );
}
}
void DIALOG_GIT_REPOSITORY::OnUpdateUI( wxUpdateUIEvent& event )
{
// event.Enable( !m_txtName->GetValue().IsEmpty() && !m_txtURL->GetValue().IsEmpty() );
}
void DIALOG_GIT_REPOSITORY::SetEncrypted( bool aEncrypted )
{
if( aEncrypted )
{
m_txtPassword->Enable();
m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) );
}
else
{
m_txtPassword->SetValue( wxEmptyString );
m_txtPassword->SetToolTip( wxEmptyString );
m_txtPassword->Disable();
}
}
std::tuple<bool,wxString,wxString,wxString> DIALOG_GIT_REPOSITORY::isValidHTTPS( const wxString& url )
{
wxRegEx regex( R"((https?:\/\/)(([^:]+)(:([^@]+))?@)?([^\/]+\/[^\s]+))" );
if( regex.Matches( url ) )
{
wxString username = regex.GetMatch( url, 3 );
wxString password = regex.GetMatch( url, 5 );
wxString repoAddress = regex.GetMatch( url, 1 ) + regex.GetMatch( url, 6 );
return std::make_tuple( true, username, password, repoAddress );
}
return std::make_tuple( false, "", "", "" );
}
std::tuple<bool,wxString, wxString> DIALOG_GIT_REPOSITORY::isValidSSH( const wxString& url )
{
wxRegEx regex( R"((?:ssh:\/\/)?([^@]+)@([^\/]+\/[^\s]+))" );
if( regex.Matches( url ) )
{
wxString username = regex.GetMatch( url, 1 );
wxString repoAddress = regex.GetMatch( url, 2 );
return std::make_tuple( true, username, repoAddress );
}
return std::make_tuple( false, "", "" );
}
static wxString get_repo_name( wxString& aRepoAddr )
{
wxString retval;
size_t last_slash = aRepoAddr.find_last_of( '/' );
bool ends_with_dot_git = aRepoAddr.EndsWith( ".git" );
if( ends_with_dot_git )
retval = aRepoAddr.substr( last_slash + 1, aRepoAddr.size() - last_slash - 5 );
else
retval = aRepoAddr.substr( last_slash + 1, aRepoAddr.size() - last_slash );
return retval;
}
void DIALOG_GIT_REPOSITORY::OnLocationExit( wxFocusEvent& event )
{
updateURLData();
updateAuthControls();
event.Skip();
}
void DIALOG_GIT_REPOSITORY::updateURLData()
{
wxString url = m_txtURL->GetValue();
if( url.IsEmpty() )
return;
if( url.Contains( "https://" ) || url.Contains( "http://" ) )
{
auto [valid, username, password, repoAddress] = isValidHTTPS( url );
if( valid )
{
m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) );
SetUsername( username );
SetPassword( password );
m_txtURL->SetValue( repoAddress );
if( m_txtName->GetValue().IsEmpty() )
m_txtName->SetValue( get_repo_name( repoAddress ) );
}
}
else if( url.Contains( "ssh://" ) || url.Contains( "git@" ) )
{
auto [valid, username, repoAddress] = isValidSSH( url );
if( valid )
{
m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) );
m_txtUsername->SetValue( username );
m_txtURL->SetValue( repoAddress );
if( m_txtName->GetValue().IsEmpty() )
m_txtName->SetValue( get_repo_name( repoAddress ) );
setDefaultSSHKey();
}
}
}
void DIALOG_GIT_REPOSITORY::OnTestClick( wxCommandEvent& event )
{
git_remote* remote = nullptr;
git_remote_callbacks callbacks;
git_remote_init_callbacks( &callbacks, GIT_REMOTE_CALLBACKS_VERSION );
// We track if we have already tried to connect.
// If we have, the server may come back to offer another connection
// type, so we need to keep track of how many times we have tried.
m_tested = 0;
callbacks.credentials = []( git_cred** aOut, const char* aUrl, const char* aUsername,
unsigned int aAllowedTypes, void* aPayload ) -> int
{
DIALOG_GIT_REPOSITORY* dialog = static_cast<DIALOG_GIT_REPOSITORY*>( aPayload );
if( dialog->GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL )
return GIT_PASSTHROUGH;
if( aAllowedTypes & GIT_CREDTYPE_USERNAME
&& !( dialog->GetTested() & GIT_CREDTYPE_USERNAME ) )
{
wxString username = dialog->GetUsername().Trim().Trim( false );
git_cred_username_new( aOut, username.ToStdString().c_str() );
dialog->GetTested() |= GIT_CREDTYPE_USERNAME;
}
else if( dialog->GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS
&& ( aAllowedTypes & GIT_CREDTYPE_USERPASS_PLAINTEXT )
&& !( dialog->GetTested() & GIT_CREDTYPE_USERPASS_PLAINTEXT ) )
{
wxString username = dialog->GetUsername().Trim().Trim( false );
wxString password = dialog->GetPassword().Trim().Trim( false );
git_cred_userpass_plaintext_new( aOut, username.ToStdString().c_str(),
password.ToStdString().c_str() );
dialog->GetTested() |= GIT_CREDTYPE_USERPASS_PLAINTEXT;
}
else if( dialog->GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH
&& ( aAllowedTypes & GIT_CREDTYPE_SSH_KEY )
&& !( dialog->GetTested() & GIT_CREDTYPE_SSH_KEY ) )
{
// SSH key authentication
wxString sshKey = dialog->GetRepoSSHPath();
wxString sshPubKey = sshKey + ".pub";
wxString username = dialog->GetUsername().Trim().Trim( false );
wxString password = dialog->GetPassword().Trim().Trim( false );
git_cred_ssh_key_new( aOut, username.ToStdString().c_str(),
sshPubKey.ToStdString().c_str(), sshKey.ToStdString().c_str(),
password.ToStdString().c_str() );
dialog->GetTested() |= GIT_CREDTYPE_SSH_KEY;
}
else
{
return GIT_PASSTHROUGH;
}
return GIT_OK;
};
callbacks.payload = this;
wxString txtURL = m_txtURL->GetValue();
git_remote_create_with_fetchspec( &remote, m_repository, "origin", txtURL.ToStdString().c_str(),
"+refs/heads/*:refs/remotes/origin/*" );
if( git_remote_connect( remote, GIT_DIRECTION_FETCH, &callbacks, nullptr, nullptr ) != GIT_OK )
SetTestResult( true, git_error_last()->message );
else
SetTestResult( false, wxEmptyString );
git_remote_disconnect( remote );
git_remote_free( remote );
auto dlg = wxMessageDialog( this, wxEmptyString, _( "Test connection" ), wxOK | wxICON_INFORMATION );
if( !m_failedTest )
{
dlg.SetMessage( _( "Connection successful" ) );
}
else
{
dlg.SetMessage( wxString::Format( _( "Could not connect to '%s' " ), m_txtURL->GetValue() ) );
dlg.SetExtendedMessage( m_testError );
}
dlg.ShowModal();
}
void DIALOG_GIT_REPOSITORY::OnFileUpdated( wxFileDirPickerEvent& aEvent )
{
wxString file = aEvent.GetPath();
if( file.ends_with( wxS( ".pub" ) ) )
file = file.Left( file.size() - 4 );
std::ifstream ifs( file.fn_str() );
if( !ifs.good() || !ifs.is_open() )
{
DisplayErrorMessage( this, wxString::Format( _( "Could not open private key '%s'" ), file ),
wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
return;
}
std::string line;
std::getline( ifs, line );
bool isValid = ( line.find( "PRIVATE KEY" ) != std::string::npos );
bool isEncrypted = ( line.find( "ENCRYPTED" ) != std::string::npos );
if( !isValid )
{
DisplayErrorMessage( this, _( "Invalid SSH Key" ),
_( "The selected file is not a valid SSH private key" ) );
CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
return;
}
if( isEncrypted )
{
m_txtPassword->Enable();
m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) );
}
else
{
m_txtPassword->SetValue( wxEmptyString );
m_txtPassword->SetToolTip( wxEmptyString );
m_txtPassword->Disable();
}
ifs.close();
wxString pubFile = file + wxS( ".pub" );
std::ifstream pubIfs( pubFile.fn_str() );
if( !pubIfs.good() || !pubIfs.is_open() )
{
DisplayErrorMessage( this, wxString::Format( _( "Could not open public key '%s'" ),
file + ".pub" ),
wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
aEvent.SetPath( wxEmptyString );
CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
return;
}
m_prevFile = file;
pubIfs.close();
}
void DIALOG_GIT_REPOSITORY::OnOKClick( wxCommandEvent& event )
{
// Save the repository details
if( m_txtName->GetValue().IsEmpty() )
{
DisplayErrorMessage( this, _( "Missing information" ),
_( "Please enter a name for the repository" ) );
return;
}
if( m_txtURL->GetValue().IsEmpty() )
{
DisplayErrorMessage( this, _( "Missing information" ),
_( "Please enter a URL for the repository" ) );
return;
}
EndModal( wxID_OK );
}
void DIALOG_GIT_REPOSITORY::updateAuthControls()
{
if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) )
{
m_panelAuth->Show( false );
}
else
{
m_panelAuth->Show( true );
if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) )
{
m_fpSSHKey->Show( true );
m_labelSSH->Show( true );
m_labelPass1->SetLabel( _( "SSH Key Password" ) );
}
else
{
m_fpSSHKey->Show( false );
m_labelSSH->Show( false );
m_labelPass1->SetLabel( _( "Password" ) );
setDefaultSSHKey();
}
}
Layout();
}
void DIALOG_GIT_REPOSITORY::OnSelectConnType( wxCommandEvent& event )
{
updateAuthControls();
}