From 27978e5cd36d2063405fc887ecdbcc421b30b767 Mon Sep 17 00:00:00 2001 From: Leo Franchi Date: Thu, 22 Mar 2012 14:38:40 -0400 Subject: [PATCH 1/3] Now that vlc plugins are in Frameworks/vlc/plugins, we can rename plugins to PlugIns to retain case-sensitive compatibility on osx --- admin/mac/macdeploy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/admin/mac/macdeploy.py b/admin/mac/macdeploy.py index 31b4fb798..1fbdf05d6 100755 --- a/admin/mac/macdeploy.py +++ b/admin/mac/macdeploy.py @@ -1,6 +1,7 @@ #!/usr/bin/python -# This file is part of Clementine. +# This file is part of Tomahawk. +# It was inspired in large part by the macdeploy script in Clementine. # # Clementine is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -249,7 +250,7 @@ frameworks_dir = os.path.join(bundle_dir, 'Contents', 'Frameworks') commands.append(['mkdir', '-p', frameworks_dir]) resources_dir = os.path.join(bundle_dir, 'Contents', 'Resources') commands.append(['mkdir', '-p', resources_dir]) -plugins_dir = os.path.join(bundle_dir, 'Contents', 'plugins') +plugins_dir = os.path.join(bundle_dir, 'Contents', 'PlugIns') binary = os.path.join(bundle_dir, 'Contents', 'MacOS', bundle_name) fixed_libraries = [] From 3c485949b6207efdfea00b3b8b931c7525d1e237 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 22 Mar 2012 17:57:29 -0400 Subject: [PATCH 2/3] Major restructuring to how ACLs will work. Disable dialog box again, for now. --- src/libtomahawk/aclregistry.cpp | 245 ++++++++----------------- src/libtomahawk/aclregistry.h | 67 +++---- src/libtomahawk/network/connection.cpp | 12 +- src/libtomahawk/network/connection.h | 2 +- src/libtomahawk/tomahawksettings.cpp | 12 +- src/libtomahawk/tomahawksettings.h | 4 +- 6 files changed, 125 insertions(+), 217 deletions(-) diff --git a/src/libtomahawk/aclregistry.cpp b/src/libtomahawk/aclregistry.cpp index f561490de..d3261f15c 100644 --- a/src/libtomahawk/aclregistry.cpp +++ b/src/libtomahawk/aclregistry.cpp @@ -43,9 +43,10 @@ ACLRegistry::ACLRegistry( QObject* parent ) : QObject( parent ) { s_instance = this; - qRegisterMetaType< ACLRegistry::ACL >("ACLRegistry::ACL"); + qRegisterMetaType< ACLRegistry::ACL >( "ACLRegistry::ACL" ); + qRegisterMetaType< ACLRegistry::User >( "ACLRegistry::User" ); - m_cache = TomahawkSettings::instance()->aclEntries(); + load(); } @@ -55,174 +56,77 @@ ACLRegistry::~ACLRegistry() void -ACLRegistry::isAuthorizedPeer( const QString& dbid, ACLRegistry::ACL globalType, const QString &username ) +ACLRegistry::isAuthorizedUser( const QString& dbid, const QString &username, ACLRegistry::ACL globalType ) { if ( QThread::currentThread() != TOMAHAWK_APPLICATION::instance()->thread() ) { - emit aclResult( dbid, globalType ); + emit aclResult( dbid, username, globalType ); return; } - - qDebug() << "Current cache keys =" << m_cache.keys(); - if( m_cache.contains( dbid ) ) + + bool found = false; + QMutableListIterator< ACLRegistry::User > i( m_cache ); + while ( i.hasNext() ) { - QVariantHash peerHash = m_cache[ dbid ].toHash(); - if( peerHash.contains( "global" ) ) + ACLRegistry::User user = i.next(); + foreach ( QString knowndbid, user.knownDbids ) { - registerAlias( dbid, username ); - emit aclResult( dbid, ACLRegistry::ACL( peerHash[ "global" ].toInt() ) ); + if ( dbid == knowndbid ) + { + if ( !user.knownAccountIds.contains( username ) ) + user.knownAccountIds.append( username ); + found = true; + } + } + + foreach ( QString knownaccountid, user.knownAccountIds ) + { + if ( username == knownaccountid ) + { + if ( !user.knownDbids.contains( dbid ) ) + user.knownDbids.append( dbid ); + found = true; + } + } + + if ( found ) + { + emit aclResult( dbid, username, user.acl ); + i.setValue( user ); + return; + } + } + + // User was not found, create a new user entry + ACLRegistry::User user; + user.knownDbids.append( dbid ); + user.knownAccountIds.append( username ); + if ( globalType != ACLRegistry::NotFound ) + user.acl = globalType; + else + { + ACLRegistry::ACL acl = globalType; + tDebug( LOGVERBOSE ) << "ACL is intially" << acl; + #ifndef ENABLE_HEADLESS + acl = getUserDecision( username ); + tDebug( LOGVERBOSE ) << "after getUserDecision acl is" << acl; + #endif + + if ( acl == ACLRegistry::NotFound ) + { + emit aclResult( dbid, username, acl ); return; } - if ( globalType == ACLRegistry::NotFound ) - { - emit aclResult( dbid, globalType ); - return; - } - - peerHash[ "global" ] = int( globalType ); - m_cache[ dbid ] = peerHash; - save(); - registerAlias( dbid, username ); - emit aclResult( dbid, globalType ); - return; + user.acl = acl; } - ACLRegistry::ACL acl = globalType; - tDebug( LOGVERBOSE ) << "ACL is intially" << acl; -#ifndef ENABLE_HEADLESS - acl = getUserDecision( username ); - tDebug( LOGVERBOSE ) << "after getUserDecision acl is" << acl; -#endif - - if ( acl == ACLRegistry::NotFound || acl == ACLRegistry::AllowOnce || acl == ACLRegistry::DenyOnce ) - { - emit aclResult( dbid, acl ); - return; - } - - QVariantHash peerHash; - peerHash[ "global" ] = int( acl ); - m_cache[ dbid ] = peerHash; - save(); - registerAlias( dbid, username ); - emit aclResult( dbid, acl ); + m_cache.append( user ); + emit aclResult( dbid, username, user.acl ); return; } -void -ACLRegistry::registerPeer( const QString& dbid, ACLRegistry::ACL globalType, const QString &username ) -{ - if ( QThread::currentThread() != TOMAHAWK_APPLICATION::instance()->thread() ) - return; - - if ( globalType == ACLRegistry::NotFound || globalType == ACLRegistry::DenyOnce || globalType == ACLRegistry::AllowOnce ) - return; - - QVariantHash peerHash; - if ( m_cache.contains( dbid ) ) - peerHash = m_cache[ dbid ].toHash(); - peerHash[ "global" ] = int( globalType ); - m_cache[ dbid ] = peerHash; - save(); - registerAlias( dbid, username ); -} - - -QPair< QString, ACLRegistry::ACL > -ACLRegistry::isAuthorizedUser( const QString &username, ACLRegistry::ACL globalType ) -{ - if ( QThread::currentThread() != TOMAHAWK_APPLICATION::instance()->thread() ) - return QPair< QString, ACLRegistry::ACL >( QString(), ACLRegistry::NotFound ); - - qDebug() << "Current cache keys =" << m_cache.keys(); - foreach ( QString dbid, m_cache.keys() ) - { - // qDebug() << "Looking up dbid"; - QVariantHash peerHash = m_cache[ dbid ].toHash(); - if ( !peerHash.contains( "usernames" ) ) - continue; - - if ( !peerHash[ "usernames" ].toStringList().contains( username ) ) - continue; - - if ( globalType != ACLRegistry::NotFound ) - { - peerHash[ "global" ] = int( globalType ); - m_cache[ dbid ] = peerHash; - save(); - return QPair< QString, ACLRegistry::ACL >( dbid, globalType ); - } - - return QPair< QString, ACLRegistry::ACL >( dbid, ACLRegistry::ACL( peerHash[ "global" ].toInt() ) ); - } - - return QPair< QString, ACLRegistry::ACL >( QString(), ACLRegistry::NotFound ); -} - - -void -ACLRegistry::registerAlias( const QString& dbid, const QString &username ) -{ - if ( QThread::currentThread() != TOMAHAWK_APPLICATION::instance()->thread() ) - return; - - if ( dbid.isEmpty() || username.isEmpty() ) - return; - - if ( !m_cache.contains( dbid ) ) - return; - - QVariantHash peerHash = m_cache[ dbid ].toHash(); - if ( !peerHash.contains( "usernames" ) ) - peerHash[ "usernames" ] = QStringList( username ); - else if ( !peerHash[ "usernames" ].toStringList().contains( username ) ) - peerHash[ "usernames" ] = peerHash[ "usernames" ].toStringList() + QStringList( username ); - else - return; - - m_cache[ dbid ] = peerHash; - save(); -} - - -// ACLRegistry::ACL -// ACLRegistry::isAuthorizedPath( const QString& dbid, const QString& path ) -// { -// QMutexLocker locker( &m_cacheMutex ); -// -// if( !m_cache.contains( dbid ) ) -// return ACLRegistry::NotFound; -// -// QHash< QString, ACL > peerHash = m_cache[dbid]; -// if( !peerHash.contains( path ) ) -// { -// if( peerHash.contains( "global" ) ) -// return peerHash["global"]; -// else -// return ACLRegistry::Deny; -// } -// return peerHash[path]; -// } -// -// void -// ACLRegistry::authorizePath( const QString& dbid, const QString& path, ACLRegistry::ACL type ) -// { -// TomahawkSettings *s = TomahawkSettings::instance(); -// if( !s->scannerPaths().contains( path ) ) -// { -// qDebug() << "path selected is not in our scanner path!"; -// return; -// } -// QMutexLocker locker( &m_cacheMutex ); -// QHash< QString, ACLRegistry::ACL > peerHash; -// if ( m_cache.contains( dbid ) ) -// peerHash = m_cache[dbid]; -// peerHash[path] = type; -// m_cache[dbid] = peerHash; -// } - #ifndef ENABLE_HEADLESS #include @@ -230,14 +134,13 @@ ACLRegistry::registerAlias( const QString& dbid, const QString &username ) ACLRegistry::ACL ACLRegistry::getUserDecision( const QString &username ) { + return ACLRegistry::Stream; QMessageBox msgBox; msgBox.setIcon( QMessageBox::Question ); msgBox.setText( tr( "Connect to Peer?" ) ); msgBox.setInformativeText( tr( "Another Tomahawk instance that claims to be owned by %1 is attempting to connect to you. Select whether to allow or deny this connection.\n\nRemember: Only allow peers to connect if you trust who they are and if you have the legal right for them to stream music from you.").arg( username ) ); - QPushButton *denyButton = msgBox.addButton( tr( "Deny" ), QMessageBox::HelpRole ); - QPushButton *alwaysDenyButton = msgBox.addButton( tr( "Always Deny" ), QMessageBox::YesRole ); - QPushButton *allowButton = msgBox.addButton( tr( "Allow" ), QMessageBox::NoRole ); - QPushButton *alwaysAllowButton = msgBox.addButton( tr( "Always Allow" ), QMessageBox::ActionRole ); + QPushButton *denyButton = msgBox.addButton( tr( "Deny" ), QMessageBox::YesRole ); + QPushButton *allowButton = msgBox.addButton( tr( "Allow" ), QMessageBox::ActionRole ); msgBox.setDefaultButton( allowButton ); msgBox.setEscapeButton( denyButton ); @@ -245,13 +148,9 @@ ACLRegistry::getUserDecision( const QString &username ) msgBox.exec(); if( msgBox.clickedButton() == denyButton ) - return ACLRegistry::DenyOnce; - else if( msgBox.clickedButton() == alwaysDenyButton ) return ACLRegistry::Deny; else if( msgBox.clickedButton() == allowButton ) - return ACLRegistry::AllowOnce; - else if( msgBox.clickedButton() == alwaysAllowButton ) - return ACLRegistry::Allow; + return ACLRegistry::Stream; //How could we get here? tDebug( LOGVERBOSE ) << "ERROR: returning NotFound"; @@ -261,8 +160,26 @@ ACLRegistry::getUserDecision( const QString &username ) #endif + +void +ACLRegistry::load() +{ + QVariantList entryList = TomahawkSettings::instance()->aclEntries(); + foreach ( QVariant entry, entryList ) + { + if ( !entry.isValid() || !entry.canConvert< ACLRegistry::User >() ) + continue; + ACLRegistry::User entryUser = entry.value< ACLRegistry::User >(); + m_cache.append( entryUser ); + } +} + + void ACLRegistry::save() { - TomahawkSettings::instance()->setAclEntries( m_cache ); + QVariantList entryList; + foreach ( ACLRegistry::User user, m_cache ) + entryList.append( QVariant::fromValue< ACLRegistry::User >( user ) ); + TomahawkSettings::instance()->setAclEntries( entryList ); } \ No newline at end of file diff --git a/src/libtomahawk/aclregistry.h b/src/libtomahawk/aclregistry.h index 8d6e2fcda..c9231ad90 100644 --- a/src/libtomahawk/aclregistry.h +++ b/src/libtomahawk/aclregistry.h @@ -39,46 +39,35 @@ public: static ACLRegistry* instance(); enum ACL { - Allow = 0, + NotFound = 0, Deny = 1, - NotFound = 2, - AllowOnce = 3, - DenyOnce = 4 + Read = 2, + Stream = 3 + }; + + struct User { + QString uuid; + QStringList knownDbids; + QStringList knownAccountIds; + ACL acl; + + User() + : uuid( QUuid::createUuid().toString() ) + {} + + User( QString p_uuid, QStringList p_knownDbids, QStringList p_knownAccountIds, ACL p_acl ) + : uuid( p_uuid ) + , knownDbids( p_knownDbids ) + , knownAccountIds( p_knownAccountIds ) + , acl( p_acl ) + {} }; ACLRegistry( QObject *parent = 0 ); ~ACLRegistry(); - - /** - * @brief Registers the global ACL value for this peer - * - * @param dbid DBID of peer - * @param globalType Global ACL to use for this peer. ACLRegistry::NotFound is invalid and will return immediately. - * @param username If not empty, will store the given username along with the new ACL value. Defaults to QString(). - * @return void - **/ - void registerPeer( const QString &dbid, ACLRegistry::ACL globalType, const QString &username = QString() ); - - /** - * @brief Checks if peer is authorized, using the username. Optionally, can change authorization of the peer, but only if the peer is found. - * - * @param username Username for the peer - * @param globalType Global ACL to store if peer is found; if ACLRegistry::NotFound, does not change the ACL. Defaults to ACLRegistry::NotFound. - * @return QPair< QString, ACLRegistry::ACL > - **/ - QPair< QString, ACLRegistry::ACL > isAuthorizedUser( const QString &username, ACLRegistry::ACL globalType = ACLRegistry::NotFound ); - - /** - * @brief Registers an alias for a known peer. If you do not know the DBID, you can retrieve it via isAuthorizedUser first. - * - * @param dbid DBID of peer - * @param username Username of the peer to be added to the entry - * @return void - **/ - void registerAlias( const QString &dbid, const QString &username ); signals: - void aclResult( QString nodeid, ACLRegistry::ACL peerStatus ); + void aclResult( QString nodeid, QString username, ACLRegistry::ACL peerStatus ); public slots: /** @@ -89,16 +78,13 @@ public slots: * @param username If not empty, will store the given username along with the new ACL value. Defaults to QString(). * @return ACLRegistry::ACL **/ - void isAuthorizedPeer( const QString &dbid, ACLRegistry::ACL globalType = ACLRegistry::NotFound, const QString &username = QString() ); + void isAuthorizedUser( const QString &dbid, const QString &username, ACLRegistry::ACL globalType = ACLRegistry::NotFound ); #ifndef ENABLE_HEADLESS ACLRegistry::ACL getUserDecision( const QString &username ); #endif -// ACLRegistry::ACL isAuthorizedPath( const QString &dbid, const QString &path ); -// void authorizePath( const QString &dbid, const QString &path, ACLRegistry::ACL type ); - private: /** * @brief Saves the cache. @@ -107,9 +93,14 @@ private: **/ void save(); - QVariantHash m_cache; + void load(); + + QList< ACLRegistry::User > m_cache; static ACLRegistry* s_instance; }; +Q_DECLARE_METATYPE( ACLRegistry::ACL ); +Q_DECLARE_METATYPE( ACLRegistry::User ); + #endif // TOMAHAWK_ACLREGISTRY_H diff --git a/src/libtomahawk/network/connection.cpp b/src/libtomahawk/network/connection.cpp index 8da4c9418..f162b7d51 100644 --- a/src/libtomahawk/network/connection.cpp +++ b/src/libtomahawk/network/connection.cpp @@ -199,20 +199,20 @@ Connection::checkACL() QString nodeid = property( "nodeid" ).toString(); tDebug( LOGVERBOSE ) << "Checking ACL for" << name(); - connect( ACLRegistry::instance(), SIGNAL( aclResult( QString, ACLRegistry::ACL ) ), this, SLOT( checkACLResult( QString, ACLRegistry::ACL ) ), Qt::QueuedConnection ); - QMetaObject::invokeMethod( ACLRegistry::instance(), "isAuthorizedPeer", Qt::QueuedConnection, Q_ARG( QString, nodeid ), Q_ARG( ACLRegistry::ACL, ACLRegistry::NotFound ), Q_ARG( QString, name() ) ); + connect( ACLRegistry::instance(), SIGNAL( aclResult( QString, QString, ACLRegistry::ACL ) ), this, SLOT( checkACLResult( QString, QString, ACLRegistry::ACL ) ), Qt::QueuedConnection ); + QMetaObject::invokeMethod( ACLRegistry::instance(), "isAuthorizedUser", Qt::QueuedConnection, Q_ARG( QString, nodeid ), Q_ARG( QString, name() ), Q_ARG( ACLRegistry::ACL, ACLRegistry::NotFound ) ); } void -Connection::checkACLResult( const QString &nodeid, ACLRegistry::ACL peerStatus ) +Connection::checkACLResult( const QString &nodeid, const QString &username, ACLRegistry::ACL peerStatus ) { - if ( nodeid != property( "nodeid" ).toString() ) + if ( nodeid != property( "nodeid" ).toString() || username != name() ) return; - disconnect( ACLRegistry::instance(), SIGNAL( aclResult( QString, ACLRegistry::ACL ) ) ); + disconnect( ACLRegistry::instance(), SIGNAL( aclResult( QString, QString, ACLRegistry::ACL ) ) ); tDebug( LOGVERBOSE ) << "ACL status is" << peerStatus; - if ( peerStatus == ACLRegistry::Allow || peerStatus == ACLRegistry::AllowOnce ) + if ( peerStatus == ACLRegistry::Stream ) { QTimer::singleShot( 0, this, SLOT( doSetup() ) ); return; diff --git a/src/libtomahawk/network/connection.h b/src/libtomahawk/network/connection.h index 275688da7..d5b5e93b2 100644 --- a/src/libtomahawk/network/connection.h +++ b/src/libtomahawk/network/connection.h @@ -120,7 +120,7 @@ private slots: void readyRead(); void doSetup(); void checkACL(); - void checkACLResult( const QString &id, ACLRegistry::ACL peerStatus ); + void checkACLResult( const QString &nodeid, const QString &username, ACLRegistry::ACL peerStatus ); void authCheckTimeout(); void bytesWritten( qint64 ); void calcStats(); diff --git a/src/libtomahawk/tomahawksettings.cpp b/src/libtomahawk/tomahawksettings.cpp index 58eded73c..ff40668fb 100644 --- a/src/libtomahawk/tomahawksettings.cpp +++ b/src/libtomahawk/tomahawksettings.cpp @@ -628,19 +628,19 @@ TomahawkSettings::setProxyDns( bool lookupViaProxy ) } -QVariantHash +QVariantList TomahawkSettings::aclEntries() const { - QVariant retVal = value( "acl/entries", QVariantHash() ); - if ( retVal.isValid() && retVal.canConvert< QVariantHash >() ) - return retVal.toHash(); + QVariant retVal = value( "acl/entries", QVariantList() ); + if ( retVal.isValid() && retVal.canConvert< QVariantList >() ) + return retVal.toList(); - return QVariantHash(); + return QVariantList(); } void -TomahawkSettings::setAclEntries( const QVariantHash &entries ) +TomahawkSettings::setAclEntries( const QVariantList &entries ) { setValue( "acl/entries", entries ); } diff --git a/src/libtomahawk/tomahawksettings.h b/src/libtomahawk/tomahawksettings.h index 3cdfe7b45..5200bc524 100644 --- a/src/libtomahawk/tomahawksettings.h +++ b/src/libtomahawk/tomahawksettings.h @@ -163,8 +163,8 @@ public: void setProxyDns( bool lookupViaProxy ); /// ACL settings - QVariantHash aclEntries() const; - void setAclEntries( const QVariantHash &entries ); + QVariantList aclEntries() const; + void setAclEntries( const QVariantList &entries ); /// XMPP Component Settings QString xmppBotServer() const; From f41872540d7f0a9dcc55978208d39d02b1b5ba6c Mon Sep 17 00:00:00 2001 From: Jason Herskowitz Date: Sat, 24 Mar 2012 20:23:20 -0400 Subject: [PATCH 3/3] Tweak recently played icon to give more padding --- data/images/recently-played.png | Bin 5175 -> 8766 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/images/recently-played.png b/data/images/recently-played.png index 956f761f22bd9fe2e26c5746aaa463fc4051ddf0..0c1161af09896ce4169c076a4fd9402b561e8194 100644 GIT binary patch delta 6120 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

su00W{(lN|k5qtXhiXFvqZxKK z*Uj=xwn!|kqX&2<3;`hEAq0XE{9g9~LJ{B@;6D;mYe)h=&E^NQ6(CKBCsqpu>HIZ? zDFDDDz=B0vl3ShA$V+bDhxJQwtra^0fxN8mRSb7Odm91r#y$sVQK zfp;bLM(|CBdFYc50a}3vCH9f?I9bg3=R<&jc4xhmL1Y{T?ik4%j8uRVz!!;GMiEO$ z>Dcd!QUHKX;5yXq6QOY2K?tk0!rG|*I&hnmNx@hPtbY#Ujl#@SutwjQkYDx!Uk&Tc z!cJAO9Qd`AQQmkT_!Q7)ssJfqCBv*l6>_2tbt3rTFyAy^$!b;ryQIuQqaFAh>R}Hy z2+*R3D7&TXLMi-}oec`CGg|}iG8Wq0;aggQ-WxbO8^cj<353 z^uz8caG4%Xo(+sIK=ZU0 zcnP%)yUDgfw_SjfIiW`n14*oEb!Tg~mf=jG8mQ8LYdKIc%zLI$6IH%$g1Uk2y3m{S z`28R;A#R694^+onfYX4FYXK$!rCNmQEI0%>r2jpld+pm%bM+s_ZEWiJzGQ?iW?Tt@ h2q1_6Lb3AS0RVCE_OF$8mNEbU002ovPDHLkV1no9hQ0s*