Imported Upstream version 14.45.0
[platform/upstream/libzypp.git] / zypp / RepoManager.cc
index 4c64ee4..3853444 100644 (file)
@@ -21,6 +21,7 @@
 #include "zypp/base/InputStream.h"
 #include "zypp/base/LogTools.h"
 #include "zypp/base/Gettext.h"
+#include "zypp/base/DefaultIntegral.h"
 #include "zypp/base/Function.h"
 #include "zypp/base/Regex.h"
 #include "zypp/PathInfo.h"
@@ -63,6 +64,76 @@ namespace zypp
   ///////////////////////////////////////////////////////////////////
   namespace
   {
+    ///////////////////////////////////////////////////////////////////
+    /// \class UrlCredentialExtractor
+    /// \brief Extract credentials in \ref Url authority and store them via \ref CredentialManager.
+    ///
+    /// Lazy init CredentialManager and save collected credentials when
+    /// going out of scope.
+    ///
+    /// Methods return whether a password has been collected/extracted.
+    ///
+    /// \code
+    /// UrlCredentialExtractor( "/rootdir" ).collect( oneUrlOrUrlContainer );
+    /// \endcode
+    /// \code
+    /// {
+    ///   UrlCredentialExtractor extractCredentials;
+    ///   extractCredentials.collect( oneUrlOrUrlContainer );
+    ///   extractCredentials.extract( oneMoreUrlOrUrlContainer );
+    ///   ....
+    /// }
+    /// \endcode
+    ///
+    class UrlCredentialExtractor
+    {
+    public:
+      UrlCredentialExtractor( Pathname & root_r )
+      : _root( root_r )
+      {}
+
+      ~UrlCredentialExtractor()
+      { if ( _cmPtr ) _cmPtr->save(); }
+
+      /** Remember credentials stored in URL authority leaving the password in \a url_r. */
+      bool collect( const Url & url_r )
+      {
+       bool ret = url_r.hasCredentialsInAuthority();
+       if ( ret )
+       {
+         if ( !_cmPtr ) _cmPtr.reset( new media::CredentialManager( _root ) );
+         _cmPtr->addUserCred( url_r );
+       }
+       return ret;
+      }
+      /** \overload operating on Url container */
+      template<class TContainer>
+      bool collect( const TContainer & urls_r )
+      {        bool ret = false; for ( const Url & url : urls_r ) { if ( collect( url ) && !ret ) ret = true; } return ret; }
+
+      /** Remember credentials stored in URL authority stripping the passowrd from \a url_r. */
+      bool extract( Url & url_r )
+      {
+       bool ret = collect( url_r );
+       if ( ret )
+         url_r.setPassword( std::string() );
+       return ret;
+      }
+      /** \overload operating on Url container */
+      template<class TContainer>
+      bool extract( TContainer & urls_r )
+      {        bool ret = false; for ( Url & url : urls_r ) { if ( extract( url ) && !ret ) ret = true; } return ret; }
+
+    private:
+      const Pathname & _root;
+      scoped_ptr<media::CredentialManager> _cmPtr;
+    };
+  } // namespace
+  ///////////////////////////////////////////////////////////////////
+
+  ///////////////////////////////////////////////////////////////////
+  namespace
+  {
     /** Simple media mounter to access non-downloading URLs e.g. for non-local plaindir repos.
      * \ingroup g_RAII
      */
@@ -148,7 +219,7 @@ namespace zypp
     /**
      * \short Simple callback to collect the results
      *
-     * Classes like RepoFileParser call the callback
+     * Classes like RepoFileReader call the callback
      * once per each repo in a file.
      *
      * Passing this functor as callback, you can collect
@@ -203,7 +274,7 @@ namespace zypp
       MIL << "repo file: " << file << endl;
       RepoCollector collector;
       parser::RepoFileReader parser( file, bind( &RepoCollector::collect, &collector, _1 ) );
-      return collector.repos;
+      return std::move(collector.repos);
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -220,23 +291,35 @@ namespace zypp
     {
       MIL << "directory " << dir << endl;
       std::list<RepoInfo> repos;
-      std::list<Pathname> entries;
-      if ( filesystem::readdir( entries, dir, false ) != 0 )
+      bool nonroot( geteuid() != 0 );
+      if ( nonroot && ! PathInfo(dir).userMayRX() )
       {
-       // TranslatorExplanation '%s' is a pathname
-       ZYPP_THROW(Exception(str::form(_("Failed to read directory '%s'"), dir.c_str())));
+       JobReport::warning( str::FormatNAC(_("Cannot read repo directory '%1%': Permission denied")) % dir );
       }
-
-      str::regex allowedRepoExt("^\\.repo(_[0-9]+)?$");
-      for ( std::list<Pathname>::const_iterator it = entries.begin(); it != entries.end(); ++it )
+      else
       {
-       if (str::regex_match(it->extension(), allowedRepoExt))
+       std::list<Pathname> entries;
+       if ( filesystem::readdir( entries, dir, false ) != 0 )
        {
-         std::list<RepoInfo> tmp = repositories_in_file( *it );
-         repos.insert( repos.end(), tmp.begin(), tmp.end() );
+         // TranslatorExplanation '%s' is a pathname
+         ZYPP_THROW(Exception(str::form(_("Failed to read directory '%s'"), dir.c_str())));
+       }
 
-         //std::copy( collector.repos.begin(), collector.repos.end(), std::back_inserter(repos));
-         //MIL << "ok" << endl;
+       str::regex allowedRepoExt("^\\.repo(_[0-9]+)?$");
+       for ( std::list<Pathname>::const_iterator it = entries.begin(); it != entries.end(); ++it )
+       {
+         if ( str::regex_match(it->extension(), allowedRepoExt) )
+         {
+           if ( nonroot && ! PathInfo(*it).userMayR() )
+           {
+             JobReport::warning( str::FormatNAC(_("Cannot read repo file '%1%': Permission denied")) % *it );
+           }
+           else
+           {
+             const std::list<RepoInfo> & tmp( repositories_in_file( *it ) );
+             repos.insert( repos.end(), tmp.begin(), tmp.end() );
+           }
+         }
        }
       }
       return repos;
@@ -247,7 +330,7 @@ namespace zypp
     inline void assert_alias( const RepoInfo & info )
     {
       if ( info.alias().empty() )
-       ZYPP_THROW( RepoNoAliasException() );
+       ZYPP_THROW( RepoNoAliasException( info ) );
       // bnc #473834. Maybe we can match the alias against a regex to define
       // and check for valid aliases
       if ( info.alias()[0] == '.')
@@ -258,7 +341,7 @@ namespace zypp
     inline void assert_alias( const ServiceInfo & info )
     {
       if ( info.alias().empty() )
-       ZYPP_THROW( ServiceNoAliasException() );
+       ZYPP_THROW( ServiceNoAliasException( info ) );
       // bnc #473834. Maybe we can match the alias against a regex to define
       // and check for valid aliases
       if ( info.alias()[0] == '.')
@@ -399,6 +482,21 @@ namespace zypp
     return ret;
   }
 
+  std:: ostream & operator<<( std::ostream & str, const RepoManagerOptions & obj )
+  {
+#define OUTS(X) str << "  " #X "\t" << obj.X << endl
+    str << "RepoManagerOptions (" << obj.rootDir << ") {" << endl;
+    OUTS( repoRawCachePath );
+    OUTS( repoSolvCachePath );
+    OUTS( repoPackagesCachePath );
+    OUTS( knownReposPath );
+    OUTS( knownServicesPath );
+    OUTS( pluginsPath );
+    str << "}" << endl;
+#undef OUTS
+    return str;
+  }
+
   ///////////////////////////////////////////////////////////////////
   /// \class RepoManager::Impl
   /// \brief RepoManager implementation.
@@ -414,19 +512,62 @@ namespace zypp
       init_knownRepositories();
     }
 
+    ~Impl()
+    {
+      // trigger appdata refresh if some repos change
+      if ( _reposDirty && geteuid() == 0 && ( _options.rootDir.empty() || _options.rootDir == "/" ) )
+      {
+       try {
+         std::list<Pathname> entries;
+         filesystem::readdir( entries, _options.pluginsPath/"appdata", false );
+         if ( ! entries.empty() )
+         {
+           ExternalProgram::Arguments cmd;
+           cmd.push_back( "<" );               // discard stdin
+           cmd.push_back( ">" );               // discard stdout
+           cmd.push_back( "PROGRAM" );         // [2] - fix index below if changing!
+           for ( const auto & rinfo : repos() )
+           {
+             if ( ! rinfo.enabled() )
+               continue;
+             cmd.push_back( "-R" );
+             cmd.push_back( rinfo.alias() );
+             cmd.push_back( "-t" );
+             cmd.push_back( rinfo.type().asString() );
+             cmd.push_back( "-p" );
+             cmd.push_back( rinfo.metadataPath().asString() );
+           }
+
+           for_( it, entries.begin(), entries.end() )
+           {
+             PathInfo pi( *it );
+             //DBG << "/tmp/xx ->" << pi << endl;
+             if ( pi.isFile() && pi.userMayRX() )
+             {
+               // trigger plugin
+               cmd[2] = pi.asString();         // [2] - PROGRAM
+               ExternalProgram prog( cmd, ExternalProgram::Stderr_To_Stdout );
+             }
+           }
+         }
+       }
+       catch (...) {}  // no throw in dtor
+      }
+    }
+
   public:
-    bool repoEmpty() const             { return _repos.empty(); }
-    RepoSizeType repoSize() const      { return _repos.size(); }
-    RepoConstIterator repoBegin() const        { return _repos.begin(); }
-    RepoConstIterator repoEnd() const  { return _repos.end(); }
+    bool repoEmpty() const             { return repos().empty(); }
+    RepoSizeType repoSize() const      { return repos().size(); }
+    RepoConstIterator repoBegin() const        { return repos().begin(); }
+    RepoConstIterator repoEnd() const  { return repos().end(); }
 
     bool hasRepo( const std::string & alias ) const
-    { return foundAliasIn( alias, _repos ); }
+    { return foundAliasIn( alias, repos() ); }
 
     RepoInfo getRepo( const std::string & alias ) const
     {
-      RepoConstIterator it( findAlias( alias, _repos ) );
-      return it == _repos.end() ? RepoInfo::noRepo : *it;
+      RepoConstIterator it( findAlias( alias, repos() ) );
+      return it == repos().end() ? RepoInfo::noRepo : *it;
     }
 
   public:
@@ -449,6 +590,7 @@ namespace zypp
     void buildCache( const RepoInfo & info, CacheBuildPolicy policy, OPT_PROGRESS );
 
     repo::RepoType probe( const Url & url, const Pathname & path = Pathname() ) const;
+    repo::RepoType probeCache( const Pathname & path_r ) const;
 
     void cleanCacheDirGarbage( OPT_PROGRESS );
 
@@ -497,11 +639,11 @@ namespace zypp
     void removeService( const ServiceInfo & service )
     { removeService( service.alias() ); }
 
-    void refreshServices();
+    void refreshServices( const RefreshServiceOptions & options_r );
 
-    void refreshService( const std::string & alias );
-    void refreshService( const ServiceInfo & service )
-    {  refreshService( service.alias() ); }
+    void refreshService( const std::string & alias, const RefreshServiceOptions & options_r );
+    void refreshService( const ServiceInfo & service, const RefreshServiceOptions & options_r )
+    {  refreshService( service.alias(), options_r ); }
 
     void modifyService( const std::string & oldAlias, const ServiceInfo & newService );
 
@@ -531,8 +673,8 @@ namespace zypp
     void getRepositoriesInService( const std::string & alias, OutputIterator out ) const
     {
       MatchServiceAlias filter( alias );
-      std::copy( boost::make_filter_iterator( filter, _repos.begin(), _repos.end() ),
-                 boost::make_filter_iterator( filter, _repos.end(), _repos.end() ),
+      std::copy( boost::make_filter_iterator( filter, repos().begin(), repos().end() ),
+                 boost::make_filter_iterator( filter, repos().end(), repos().end() ),
                  out);
     }
 
@@ -540,11 +682,16 @@ namespace zypp
     void init_knownServices();
     void init_knownRepositories();
 
+    const RepoSet & repos() const { return _reposX; }
+    RepoSet & reposManip()        { if ( ! _reposDirty ) _reposDirty = true; return _reposX; }
+
   private:
     RepoManagerOptions _options;
-    RepoSet            _repos;
+    RepoSet            _reposX;
     ServiceSet         _services;
 
+    DefaultIntegral<bool,false> _reposDirty;
+
   private:
     friend Impl * rwcowClone<Impl>( const Impl * rhs );
     /** clone for RWCOW_pointer */
@@ -630,6 +777,37 @@ namespace zypp
     repo::PluginServices(_options.pluginsPath/"services", ServiceCollector(_services));
   }
 
+  ///////////////////////////////////////////////////////////////////
+  namespace {
+    /** Delete \a cachePath_r subdirs not matching known aliases in \a repoEscAliases_r (must be sorted!)
+     * \note bnc#891515: Auto-cleanup only zypp.conf default locations. Otherwise
+     * we'd need some magic file to identify zypp cache directories. Without this
+     * we may easily remove user data (zypper --pkg-cache-dir . download ...)
+     */
+    inline void cleanupNonRepoMetadtaFolders( const Pathname & cachePath_r,
+                                             const Pathname & defaultCachePath_r,
+                                             const std::list<std::string> & repoEscAliases_r )
+    {
+      if ( cachePath_r != defaultCachePath_r )
+       return;
+
+      std::list<std::string> entries;
+      if ( filesystem::readdir( entries, cachePath_r, false ) == 0 )
+      {
+       entries.sort();
+       std::set<std::string> oldfiles;
+       set_difference( entries.begin(), entries.end(), repoEscAliases_r.begin(), repoEscAliases_r.end(),
+                       std::inserter( oldfiles, oldfiles.end() ) );
+       for ( const std::string & old : oldfiles )
+       {
+         if ( old == Repository::systemRepoAlias() )   // don't remove the @System solv file
+           continue;
+         filesystem::recursive_rmdir( cachePath_r / old );
+       }
+      }
+    }
+  } // namespace
+  ///////////////////////////////////////////////////////////////////
   void RepoManager::Impl::init_knownRepositories()
   {
     MIL << "start construct known repos" << endl;
@@ -637,37 +815,60 @@ namespace zypp
     if ( PathInfo(_options.knownReposPath).isExist() )
     {
       std::list<std::string> repoEscAliases;
+      std::list<RepoInfo> orphanedRepos;
       for ( RepoInfo & repoInfo : repositories_in_dir(_options.knownReposPath) )
       {
         // set the metadata path for the repo
         repoInfo.setMetadataPath( rawcache_path_for_repoinfo(_options, repoInfo) );
        // set the downloaded packages path for the repo
        repoInfo.setPackagesPath( packagescache_path_for_repoinfo(_options, repoInfo) );
+       // remember it
+        _reposX.insert( repoInfo );    // direct access via _reposX in ctor! no reposManip.
+
+       // detect orphaned repos belonging to a deleted service
+       const std::string & serviceAlias( repoInfo.service() );
+       if ( ! ( serviceAlias.empty() || hasService( serviceAlias ) ) )
+       {
+         WAR << "Schedule orphaned service repo for deletion: " << repoInfo << endl;
+         orphanedRepos.push_back( repoInfo );
+         continue;     // don't remember it in repoEscAliases
+       }
 
-        _repos.insert( repoInfo );
         repoEscAliases.push_back(repoInfo.escaped_alias());
       }
-      repoEscAliases.sort();
 
-      // delete metadata folders without corresponding repo (e.g. old tmp directories)
-      for ( const Pathname & cachePath : { _options.repoRawCachePath
-                                        , _options.repoSolvCachePath } )
+      // Cleanup orphanded service repos:
+      if ( ! orphanedRepos.empty() )
       {
-       std::list<std::string> entries;
-       if ( filesystem::readdir( entries, cachePath, false ) == 0 )
+       for ( auto & repoInfo : orphanedRepos )
        {
-         entries.sort();
-         std::set<std::string> oldfiles;
-         set_difference( entries.begin(), entries.end(), repoEscAliases.begin(), repoEscAliases.end(),
-                         std::inserter( oldfiles, oldfiles.end() ) );
-         for ( const std::string & old : oldfiles )
+         MIL << "Delete orphaned service repo " << repoInfo.alias() << endl;
+         // translators: Cleanup a repository previously owned by a meanwhile unknown (deleted) service.
+         //   %1% = service name
+         //   %2% = repository name
+         JobReport::warning( str::FormatNAC(_("Unknown service '%1%': Removing orphaned service repository '%2%'"))
+                             % repoInfo.service()
+                             % repoInfo.alias() );
+         try {
+           removeRepository( repoInfo );
+         }
+         catch ( const Exception & caugth )
          {
-           if ( old == Repository::systemRepoAlias() ) // don't remove the @System solv file
-             continue;
-           filesystem::recursive_rmdir( cachePath / old );
+           JobReport::error( caugth.asUserHistory() );
          }
        }
       }
+
+      // delete metadata folders without corresponding repo (e.g. old tmp directories)
+      //
+      // bnc#891515: Auto-cleanup only zypp.conf default locations. Otherwise
+      // we'd need somemagic file to identify zypp cache directories. Without this
+      // we may easily remove user data (zypper --pkg-cache-dir . download ...)
+      repoEscAliases.sort();
+      RepoManagerOptions defaultCache( _options.rootDir );
+      cleanupNonRepoMetadtaFolders( _options.repoRawCachePath,         defaultCache.repoRawCachePath,          repoEscAliases );
+      cleanupNonRepoMetadtaFolders( _options.repoSolvCachePath,                defaultCache.repoSolvCachePath,         repoEscAliases );
+      cleanupNonRepoMetadtaFolders( _options.repoPackagesCachePath,    defaultCache.repoPackagesCachePath,     repoEscAliases );
     }
     MIL << "end construct known repos" << endl;
   }
@@ -682,7 +883,7 @@ namespace zypp
     RepoType repokind = info.type();
     // If unknown, probe the local metadata
     if ( repokind == RepoType::NONE )
-      repokind = probe( productdatapath.asUrl() );
+      repokind = probeCache( productdatapath );
 
     RepoStatus status;
     switch ( repokind.toEnum() )
@@ -716,7 +917,7 @@ namespace zypp
     RepoType repokind = info.type();
     if ( repokind.toEnum() == RepoType::NONE_e )
       // unknown, probe the local metadata
-      repokind = probe( productdatapath.asUrl() );
+      repokind = probeCache( productdatapath );
     // if still unknown, just return
     if (repokind == RepoType::NONE_e)
       return;
@@ -805,9 +1006,6 @@ namespace zypp
         }
       }
 
-      // To test the new matadta create temp dir as sibling of mediarootpath
-      filesystem::TmpDir tmpdir( filesystem::TmpDir::makeSibling( mediarootpath ) );
-
       repo::RepoType repokind = info.type();
       // if unknown: probe it
       if ( repokind == RepoType::NONE )
@@ -881,9 +1079,12 @@ namespace zypp
     assert_urls(info);
 
     // we will throw this later if no URL checks out fine
-    RepoException rexception(_PL("Valid metadata not found at specified URL",
-                                 "Valid metadata not found at specified URLs",
-                                info.baseUrlsSize() ) );
+    RepoException rexception( info, _PL("Valid metadata not found at specified URL",
+                                       "Valid metadata not found at specified URLs",
+                                       info.baseUrlsSize() ) );
+
+    // Suppress (interactive) media::MediaChangeReport if we in have multiple basurls (>1)
+    media::ScopedDisableMediaChangeReport guard( info.baseUrlsSize() > 1 );
 
     // try urls one by one
     for ( RepoInfo::urls_const_iterator it = info.baseUrlsBegin(); it != info.baseUrlsEnd(); ++it )
@@ -979,12 +1180,13 @@ namespace zypp
         }
         else
         {
-          ZYPP_THROW(RepoUnknownTypeException());
+          ZYPP_THROW(RepoUnknownTypeException( info ));
         }
 
         // ok we have the metadata, now exchange
         // the contents
        filesystem::exchange( tmpdir.path(), mediarootpath );
+       reposManip();   // remember to trigger appdata refresh
 
         // we are done.
         return;
@@ -999,6 +1201,9 @@ namespace zypp
         // cause of the problem of the first URL remembered
         if (it == info.baseUrlsBegin())
           rexception.remember(e);
+       else
+         rexception.addHistory(  e.asUserString() );
+
       }
     } // for every url
     ERR << "No more urls..." << endl;
@@ -1104,7 +1309,7 @@ namespace zypp
     {
       case RepoType::NONE_e:
         // unknown, probe the local metadata
-        repokind = probe( productdatapath.asUrl() );
+        repokind = probeCache( productdatapath );
       break;
       default:
       break;
@@ -1165,7 +1370,7 @@ namespace zypp
       }
       break;
       default:
-        ZYPP_THROW(RepoUnknownTypeException( _("Unhandled repository type") ));
+        ZYPP_THROW(RepoUnknownTypeException( info, _("Unhandled repository type") ));
       break;
     }
     // update timestamp and checksum
@@ -1176,6 +1381,13 @@ namespace zypp
 
   ////////////////////////////////////////////////////////////////////////////
 
+
+  /** Probe the metadata type of a repository located at \c url.
+   * Urls here may be rewritten by \ref MediaSetAccess to reflect the correct media number.
+   *
+   * \note Metadata in local cache directories must be probed using \ref probeCache as
+   * a cache path must not be rewritten (bnc#946129)
+   */
   repo::RepoType RepoManager::Impl::probe( const Url & url, const Pathname & path  ) const
   {
     MIL << "going to probe the repo type at " << url << " (" << path << ")" << endl;
@@ -1260,6 +1472,28 @@ namespace zypp
     return repo::RepoType::NONE;
   }
 
+  /** Probe Metadata in a local cache directory
+   *
+   * \note Metadata in local cache directories must not be probed using \ref probe as
+   * a cache path must not be rewritten (bnc#946129)
+   */
+  repo::RepoType RepoManager::Impl::probeCache( const Pathname & path_r ) const
+  {
+    MIL << "going to probe the cached repo at " << path_r << endl;
+
+    repo::RepoType ret = repo::RepoType::NONE;
+
+    if ( PathInfo(path_r/"/repodata/repomd.xml").isFile() )
+    { ret = repo::RepoType::RPMMD; }
+    else if ( PathInfo(path_r/"/content").isFile() )
+    { ret = repo::RepoType::YAST2; }
+    else if ( PathInfo(path_r).isDir() )
+    { ret = repo::RepoType::RPMPLAINDIR; }
+
+    MIL << "Probed cached type " << ret << " at " << path_r << endl;
+    return ret;
+  }
+
   ////////////////////////////////////////////////////////////////////////////
 
   void RepoManager::Impl::cleanCacheDirGarbage( const ProgressData::ReceiverFnc & progressrcv )
@@ -1294,7 +1528,7 @@ namespace zypp
             if ( subdir->basename() == r->escaped_alias() )
             { found = true; break; }
 
-          if ( ! found )
+          if ( ! found && ( Date::now()-PathInfo(*subdir).mtime() > Date::day ) )
             filesystem::recursive_rmdir( *subdir );
 
           progress.set( progress.val() + sdircurrent * 100 / sdircount );
@@ -1376,7 +1610,7 @@ namespace zypp
     MIL << "Try adding repo " << info << endl;
 
     RepoInfo tosave = info;
-    if ( _repos.find(tosave) != _repos.end() )
+    if ( repos().find(tosave) != repos().end() )
       ZYPP_THROW(RepoAlreadyExistsException(info));
 
     // check the first url for now
@@ -1389,7 +1623,7 @@ namespace zypp
       if ( tosave.baseUrlsSize() > 0 )
       {
         if ( probedtype == RepoType::NONE )
-          ZYPP_THROW(RepoUnknownTypeException());
+          ZYPP_THROW(RepoUnknownTypeException(info));
         else
           tosave.setType(probedtype);
       }
@@ -1423,31 +1657,14 @@ namespace zypp
       oinfo.setMetadataPath( metadataPath( tosave ) );
       oinfo.setPackagesPath( packagesPath( tosave ) );
     }
-    _repos.insert(tosave);
+    reposManip().insert(tosave);
 
     progress.set(90);
 
     // check for credentials in Urls
-    bool havePasswords = false;
-    for_( urlit, tosave.baseUrlsBegin(), tosave.baseUrlsEnd() )
-      if ( urlit->hasCredentialsInAuthority() )
-      {
-        havePasswords = true;
-        break;
-      }
-    // save the credentials
-    if ( havePasswords )
-    {
-      media::CredentialManager cm(
-          media::CredManagerOptions(_options.rootDir) );
+    UrlCredentialExtractor( _options.rootDir ).collect( tosave.baseUrls() );
 
-      for_(urlit, tosave.baseUrlsBegin(), tosave.baseUrlsEnd())
-        if (urlit->hasCredentialsInAuthority())
-          //! \todo use a method calling UI callbacks to ask where to save creds?
-          cm.saveInUser(media::AuthData(*urlit));
-    }
-
-    HistoryLog().addRepository(tosave);
+    HistoryLog(_options.rootDir).addRepository(tosave);
 
     progress.toMax();
     MIL << "done" << endl;
@@ -1501,7 +1718,7 @@ namespace zypp
       MIL << "Saving " << (*it).alias() << endl;
       it->setFilepath(repofile.asString());
       it->dumpAsIniOn(file);
-      _repos.insert(*it);
+      reposManip().insert(*it);
 
       HistoryLog(_options.rootDir).addRepository(*it);
     }
@@ -1535,21 +1752,23 @@ namespace zypp
       RepoInfo todelete = *it;
       if (todelete.filepath().empty())
       {
-        ZYPP_THROW(RepoException( _("Can't figure out where the repo is stored.") ));
+        ZYPP_THROW(RepoException( todelete, _("Can't figure out where the repo is stored.") ));
       }
       else
       {
         // figure how many repos are there in the file:
         std::list<RepoInfo> filerepos = repositories_in_file(todelete.filepath());
-        if ( (filerepos.size() == 1) && ( filerepos.front().alias() == todelete.alias() ) )
+        if ( filerepos.size() == 0     // bsc#984494: file may have already been deleted
+         ||(filerepos.size() == 1 && filerepos.front().alias() == todelete.alias() ) )
         {
-          // easy, only this one, just delete the file
-          if ( filesystem::unlink(todelete.filepath()) != 0 )
+          // easy: file does not exist, contains no or only the repo to delete: delete the file
+         int ret = filesystem::unlink( todelete.filepath() );
+          if ( ! ( ret == 0 || ret == ENOENT ) )
           {
             // TranslatorExplanation '%s' is a filename
-            ZYPP_THROW(RepoException(str::form( _("Can't delete '%s'"), todelete.filepath().c_str() )));
+            ZYPP_THROW(RepoException( todelete, str::form( _("Can't delete '%s'"), todelete.filepath().c_str() )));
           }
-          MIL << todelete.alias() << " sucessfully deleted." << endl;
+          MIL << todelete.alias() << " successfully deleted." << endl;
         }
         else
         {
@@ -1585,8 +1804,8 @@ namespace zypp
         // now delete metadata (#301037)
         cleanMetadata( todelete, mSubprogrcv );
        cleanPackages( todelete, pSubprogrcv );
-        _repos.erase(todelete);
-        MIL << todelete.alias() << " sucessfully deleted." << endl;
+        reposManip().erase(todelete);
+        MIL << todelete.alias() << " successfully deleted." << endl;
         HistoryLog(_options.rootDir).removeRepository(todelete);
         return;
       } // else filepath is empty
@@ -1611,7 +1830,7 @@ namespace zypp
 
     if (toedit.filepath().empty())
     {
-      ZYPP_THROW(RepoException( _("Can't figure out where the repo is stored.") ));
+      ZYPP_THROW(RepoException( toedit, _("Can't figure out where the repo is stored.") ));
     }
     else
     {
@@ -1645,8 +1864,10 @@ namespace zypp
       }
 
       newinfo.setFilepath(toedit.filepath());
-      _repos.erase(toedit);
-      _repos.insert(newinfo);
+      reposManip().erase(toedit);
+      reposManip().insert(newinfo);
+      // check for credentials in Urls
+      UrlCredentialExtractor( _options.rootDir ).collect( newinfo.baseUrls() );
       HistoryLog(_options.rootDir).modifyRepository(toedit, newinfo);
       MIL << "repo " << alias << " modified" << endl;
     }
@@ -1656,8 +1877,8 @@ namespace zypp
 
   RepoInfo RepoManager::Impl::getRepositoryInfo( const std::string & alias, const ProgressData::ReceiverFnc & progressrcv )
   {
-    RepoConstIterator it( findAlias( alias, _repos ) );
-    if ( it != _repos.end() )
+    RepoConstIterator it( findAlias( alias, repos() ) );
+    if ( it != repos().end() )
       return *it;
     RepoInfo info;
     info.setAlias( alias );
@@ -1700,15 +1921,8 @@ namespace zypp
     saveService( toSave );
     _services.insert( toSave );
 
-    // check for credentials in Url (username:password, not ?credentials param)
-    if ( toSave.url().hasCredentialsInAuthority() )
-    {
-      media::CredentialManager cm(
-          media::CredManagerOptions(_options.rootDir) );
-
-      //! \todo use a method calling UI callbacks to ask where to save creds?
-      cm.saveInUser(media::AuthData(toSave.url()));
-    }
+    // check for credentials in Url
+    UrlCredentialExtractor( _options.rootDir ).collect( toSave.url() );
 
     MIL << "added service " << toSave.alias() << endl;
   }
@@ -1717,14 +1931,14 @@ namespace zypp
 
   void RepoManager::Impl::removeService( const std::string & alias )
   {
-    MIL << "Going to delete repo " << alias << endl;
+    MIL << "Going to delete service " << alias << endl;
 
     const ServiceInfo & service = getService( alias );
 
     Pathname location = service.filepath();
     if( location.empty() )
     {
-      ZYPP_THROW(RepoException( _("Can't figure out where the service is stored.") ));
+      ZYPP_THROW(ServiceException( service, _("Can't figure out where the service is stored.") ));
     }
 
     ServiceSet tmpSet;
@@ -1736,9 +1950,9 @@ namespace zypp
       if ( filesystem::unlink(location) != 0 )
       {
         // TranslatorExplanation '%s' is a filename
-        ZYPP_THROW(RepoException(str::form( _("Can't delete '%s'"), location.c_str() )));
+        ZYPP_THROW(ServiceException( service, str::form( _("Can't delete '%s'"), location.c_str() ) ));
       }
-      MIL << alias << " sucessfully deleted." << endl;
+      MIL << alias << " successfully deleted." << endl;
     }
     else
     {
@@ -1757,7 +1971,7 @@ namespace zypp
           it->dumpAsIniOn(file);
       }
 
-      MIL << alias << " sucessfully deleted from file " << location <<  endl;
+      MIL << alias << " successfully deleted from file " << location <<  endl;
     }
 
     // now remove all repositories added by this service
@@ -1771,7 +1985,7 @@ namespace zypp
 
   ////////////////////////////////////////////////////////////////////////////
 
-  void RepoManager::Impl::refreshServices()
+  void RepoManager::Impl::refreshServices( const RefreshServiceOptions & options_r )
   {
     // copy the set of services since refreshService
     // can eventually invalidate the iterator
@@ -1782,14 +1996,14 @@ namespace zypp
         continue;
 
       try {
-       refreshService(*it);
+       refreshService(*it, options_r);
       }
       catch ( const repo::ServicePluginInformalException & e )
       { ;/* ignore ServicePluginInformalException */ }
     }
   }
 
-  void RepoManager::Impl::refreshService( const std::string & alias )
+  void RepoManager::Impl::refreshService( const std::string & alias, const RefreshServiceOptions & options_r )
   {
     ServiceInfo service( getService( alias ) );
     assert_alias( service );
@@ -1798,7 +2012,7 @@ namespace zypp
     // Either when probing the type, or when adjusting the repositories
     // enable/disable state.:
     bool serviceModified = false;
-    MIL << "Going to refresh service '" << service.alias() << "', url: "<< service.url() << endl;
+    MIL << "Going to refresh service '" << service.alias() << "', url: "<< service.url() << ", opts: " << options_r << endl;
 
     //! \todo add callbacks for apps (start, end, repo removed, repo added, repo changed)
 
@@ -1838,37 +2052,51 @@ namespace zypp
       uglyHack.second = e;
     }
 
+    ////////////////////////////////////////////////////////////////////////////
+    // On the fly remember the new repo states as defined the reopoindex.xml.
+    // Move into ServiceInfo later.
+    ServiceInfo::RepoStates newRepoStates;
+
     // set service alias and base url for all collected repositories
     for_( it, collector.repos.begin(), collector.repos.end() )
     {
-      // if the repo url was not set by the repoindex parser, set service's url
-      Url url;
+      // First of all: Prepend service alias:
+      it->setAlias( str::form( "%s:%s", service.alias().c_str(), it->alias().c_str() ) );
+      // set reference to the parent service
+      it->setService( service.alias() );
 
-      if ( it->baseUrlsEmpty() )
-        url = service.url();
-      else
-      {
-        // service repo can contain only one URL now, so no need to iterate.
-        url = *it->baseUrlsBegin();
-      }
+      // remember the new parsed repo state
+      newRepoStates[it->alias()] = *it;
 
-      // libzypp currently has problem with separate url + path handling
-      // so just append the path to the baseurl
+      // - If the repo url was not set by the repoindex parser, set service's url.
+      // - Libzypp currently has problem with separate url + path handling so just
+      //   append a path, if set, to the baseurls
+      // - Credentials in the url authority will be extracted later, either if the
+      //   repository is added or if we check for changed urls.
+      Pathname path;
       if ( !it->path().empty() )
       {
-        Pathname path(url.getPathName());
-        path /= it->path();
-        url.setPathName( path.asString() );
-        it->setPath("");
+       if ( it->path() != "/" )
+         path = it->path();
+       it->setPath("");
       }
 
-      // Prepend service alias:
-      it->setAlias( str::form( "%s:%s", service.alias().c_str(), it->alias().c_str() ) );
-
-      // save the url
-      it->setBaseUrl( url );
-      // set refrence to the parent service
-      it->setService( service.alias() );
+      if ( it->baseUrlsEmpty() )
+      {
+       Url url( service.rawUrl() );
+       if ( !path.empty() )
+         url.setPathName( url.getPathName() / path );
+       it->setBaseUrl( std::move(url) );
+      }
+      else if ( !path.empty() )
+      {
+       RepoInfo::url_set urls( it->rawBaseUrls() );
+       for ( Url & url : urls )
+       {
+         url.setPathName( url.getPathName() / path );
+       }
+       it->setBaseUrls( std::move(urls) );
+      }
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -1877,47 +2105,71 @@ namespace zypp
     RepoInfoList oldRepos;
     getRepositoriesInService( service.alias(), std::back_inserter( oldRepos ) );
 
+    ////////////////////////////////////////////////////////////////////////////
     // find old repositories to remove...
-    for_( it, oldRepos.begin(), oldRepos.end() )
+    for_( oldRepo, oldRepos.begin(), oldRepos.end() )
     {
-      if ( ! foundAliasIn( it->alias(), collector.repos ) )
+      if ( ! foundAliasIn( oldRepo->alias(), collector.repos ) )
       {
-        if ( it->enabled() && ! service.repoToDisableFind( it->alias() ) )
-        {
-          DBG << "Service removes enabled repo " << it->alias() << endl;
-          service.addRepoToEnable( it->alias() );
-          serviceModified = true;
-        }
-        else
-        {
-          DBG << "Service removes disabled repo " << it->alias() << endl;
-        }
-        removeRepository( *it );
+       if ( oldRepo->enabled() )
+       {
+         // Currently enabled. If this was a user modification remember the state.
+         const auto & last = service.repoStates().find( oldRepo->alias() );
+         if ( last != service.repoStates().end() && ! last->second.enabled )
+         {
+           DBG << "Service removes user enabled repo " << oldRepo->alias() << endl;
+           service.addRepoToEnable( oldRepo->alias() );
+           serviceModified = true;
+         }
+         else
+           DBG << "Service removes enabled repo " << oldRepo->alias() << endl;
+       }
+       else
+         DBG << "Service removes disabled repo " << oldRepo->alias() << endl;
+
+        removeRepository( *oldRepo );
       }
     }
 
     ////////////////////////////////////////////////////////////////////////////
-    // create missing repositories and modify exising ones if needed...
+    // create missing repositories and modify existing ones if needed...
+    UrlCredentialExtractor urlCredentialExtractor( _options.rootDir ); // To collect any credentials stored in repo URLs
     for_( it, collector.repos.begin(), collector.repos.end() )
     {
-      // Service explicitly requests the repo being enabled?
-      // Service explicitly requests the repo being disabled?
+      // User explicitly requested the repo being enabled?
+      // User explicitly requested the repo being disabled?
       // And hopefully not both ;) If so, enable wins.
-      bool beEnabled = service.repoToEnableFind( it->alias() );
-      bool beDisabled = service.repoToDisableFind( it->alias() );
 
-      // Make sure the service repo is created with the
-      // appropriate enable
-      if ( beEnabled ) it->setEnabled(true);
-      if ( beDisabled ) it->setEnabled(false);
+      TriBool toBeEnabled( indeterminate );    // indeterminate - follow the service request
+      DBG << "Service request to " << (it->enabled()?"enable":"disable") << " service repo " << it->alias() << endl;
 
-      if ( beEnabled )
+      if ( options_r.testFlag( RefreshService_restoreStatus ) )
       {
-        // Remove from enable request list.
-        // NOTE: repoToDisable is handled differently.
-        //       It gets cleared on each refresh.
-        service.delRepoToEnable( it->alias() );
-        serviceModified = true;
+       DBG << "Opt RefreshService_restoreStatus " << it->alias() << endl;
+       // this overrides any pending request!
+       // Remove from enable request list.
+       // NOTE: repoToDisable is handled differently.
+       //       It gets cleared on each refresh.
+       service.delRepoToEnable( it->alias() );
+       // toBeEnabled stays indeterminate!
+      }
+      else
+      {
+       if ( service.repoToEnableFind( it->alias() ) )
+       {
+         DBG << "User request to enable service repo " << it->alias() << endl;
+         toBeEnabled = true;
+         // Remove from enable request list.
+         // NOTE: repoToDisable is handled differently.
+         //       It gets cleared on each refresh.
+         service.delRepoToEnable( it->alias() );
+         serviceModified = true;
+       }
+       else if ( service.repoToDisableFind( it->alias() ) )
+       {
+         DBG << "User request to disable service repo " << it->alias() << endl;
+         toBeEnabled = false;
+       }
       }
 
       RepoInfoList::iterator oldRepo( findAlias( it->alias(), oldRepos ) );
@@ -1925,60 +2177,118 @@ namespace zypp
       {
         // Not found in oldRepos ==> a new repo to add
 
-        // At that point check whether a repo with the same alias
-        // exists outside this service. Maybe forcefully re-alias
-        // the existing repo?
+       // Make sure the service repo is created with the appropriate enablement
+       if ( ! indeterminate(toBeEnabled) )
+         it->setEnabled( toBeEnabled );
+
         DBG << "Service adds repo " << it->alias() << " " << (it->enabled()?"enabled":"disabled") << endl;
         addRepository( *it );
-
-        // save repo credentials
-        // ma@: task for modifyRepository?
       }
       else
       {
         // ==> an exising repo to check
         bool oldRepoModified = false;
 
+       if ( indeterminate(toBeEnabled) )
+       {
+         // No user request: check for an old user modificaton otherwise follow service request.
+         // NOTE: Assert toBeEnabled is boolean afterwards!
+         if ( oldRepo->enabled() == it->enabled() )
+           toBeEnabled = it->enabled();        // service requests no change to the system
+         else if (options_r.testFlag( RefreshService_restoreStatus ) )
+         {
+           toBeEnabled = it->enabled();        // RefreshService_restoreStatus forced
+           DBG << "Opt RefreshService_restoreStatus " << it->alias() <<  " forces " << (toBeEnabled?"enabled":"disabled") << endl;
+         }
+         else
+         {
+           const auto & last = service.repoStates().find( oldRepo->alias() );
+           if ( last == service.repoStates().end() || last->second.enabled != it->enabled() )
+             toBeEnabled = it->enabled();      // service request has changed since last refresh -> follow
+           else
+           {
+             toBeEnabled = oldRepo->enabled(); // service request unchaned since last refresh -> keep user modification
+             DBG << "User modified service repo " << it->alias() <<  " may stay " << (toBeEnabled?"enabled":"disabled") << endl;
+           }
+         }
+       }
+
         // changed enable?
-        if ( beEnabled )
-        {
-          if ( ! oldRepo->enabled() )
-          {
-            DBG << "Service repo " << it->alias() << " gets enabled" << endl;
-            oldRepo->setEnabled( true );
-            oldRepoModified = true;
-          }
-          else
-          {
-            DBG << "Service repo " << it->alias() << " stays enabled" << endl;
-          }
-        }
-        else if ( beDisabled )
-        {
-          if ( oldRepo->enabled() )
-          {
-            DBG << "Service repo " << it->alias() << " gets disabled" << endl;
-            oldRepo->setEnabled( false );
-            oldRepoModified = true;
-          }
-          else
-          {
-            DBG << "Service repo " << it->alias() << " stays disabled" << endl;
-          }
-        }
-        else
+       if ( toBeEnabled == oldRepo->enabled() )
+       {
+         DBG << "Service repo " << it->alias() << " stays " <<  (oldRepo->enabled()?"enabled":"disabled") << endl;
+       }
+       else if ( toBeEnabled )
+       {
+         DBG << "Service repo " << it->alias() << " gets enabled" << endl;
+         oldRepo->setEnabled( true );
+         oldRepoModified = true;
+       }
+       else
         {
-          DBG << "Service repo " << it->alias() << " stays " <<  (oldRepo->enabled()?"enabled":"disabled") << endl;
-        }
+         DBG << "Service repo " << it->alias() << " gets disabled" << endl;
+         oldRepo->setEnabled( false );
+         oldRepoModified = true;
+       }
+
+       // all other attributes follow the service request:
+
+       // changed name (raw!)
+       if ( oldRepo->rawName() != it->rawName() )
+       {
+         DBG << "Service repo " << it->alias() << " gets new NAME " << it->rawName() << endl;
+         oldRepo->setName( it->rawName() );
+         oldRepoModified = true;
+       }
+
+       // changed autorefresh
+       if ( oldRepo->autorefresh() != it->autorefresh() )
+       {
+         DBG << "Service repo " << it->alias() << " gets new AUTOREFRESH " << it->autorefresh() << endl;
+         oldRepo->setAutorefresh( it->autorefresh() );
+         oldRepoModified = true;
+       }
+
+       // changed priority?
+       if ( oldRepo->priority() != it->priority() )
+       {
+         DBG << "Service repo " << it->alias() << " gets new PRIORITY " << it->priority() << endl;
+         oldRepo->setPriority( it->priority() );
+         oldRepoModified = true;
+       }
 
         // changed url?
-        // service repo can contain only one URL now, so no need to iterate.
-        if ( oldRepo->url() != it->url() )
         {
-          DBG << "Service repo " << it->alias() << " gets new URL " << it->url() << endl;
-          oldRepo->setBaseUrl( it->url() );
-          oldRepoModified = true;
-        }
+         RepoInfo::url_set newUrls( it->rawBaseUrls() );
+         urlCredentialExtractor.extract( newUrls );    // Extract! to prevent passwds from disturbing the comparison below
+         if ( oldRepo->rawBaseUrls() != newUrls )
+         {
+           DBG << "Service repo " << it->alias() << " gets new URLs " << newUrls << endl;
+           oldRepo->setBaseUrls( std::move(newUrls) );
+           oldRepoModified = true;
+         }
+       }
+
+        // changed gpg check settings?
+       // ATM only plugin services can set GPG values.
+       if ( service.type() == ServiceType::PLUGIN )
+       {
+         TriBool ogpg[3];      // Gpg RepoGpg PkgGpg
+         TriBool ngpg[3];
+         oldRepo->getRawGpgChecks( ogpg[0], ogpg[1], ogpg[2] );
+         it->     getRawGpgChecks( ngpg[0], ngpg[1], ngpg[2] );
+#define Z_CHKGPG(I,N)                                                                          \
+         if ( ! sameTriboolState( ogpg[I], ngpg[I] ) )                                         \
+         {                                                                                     \
+           DBG << "Service repo " << it->alias() << " gets new "#N"Check " << ngpg[I] << endl; \
+           oldRepo->set##N##Check( ngpg[I] );                                                  \
+           oldRepoModified = true;                                                             \
+         }
+         Z_CHKGPG( 0, Gpg );
+         Z_CHKGPG( 1, RepoGpg );
+         Z_CHKGPG( 2, PkgGpg );
+#undef Z_CHKGPG
+       }
 
         // save if modified:
         if ( oldRepoModified )
@@ -1995,9 +2305,16 @@ namespace zypp
       serviceModified = true;
     }
 
+    // Remember original service request for next refresh
+    if ( service.repoStates() != newRepoStates )
+    {
+      service.setRepoStates( std::move(newRepoStates) );
+      serviceModified = true;
+    }
+
     ////////////////////////////////////////////////////////////////////////////
-    // save service if modified:
-    if ( serviceModified )
+    // save service if modified: (unless a plugin service)
+    if ( serviceModified && service.type() != ServiceType::PLUGIN )
     {
       // write out modified service file.
       modifyService( service.alias(), service );
@@ -2021,8 +2338,7 @@ namespace zypp
 
     if ( service.type() == ServiceType::PLUGIN )
     {
-        MIL << "Not modifying plugin service '" << oldAlias << "'" << endl;
-        return;
+      ZYPP_THROW(ServicePluginImmutableException( service ));
     }
 
     const ServiceInfo & oldService = getService(oldAlias);
@@ -2030,7 +2346,7 @@ namespace zypp
     Pathname location = oldService.filepath();
     if( location.empty() )
     {
-      ZYPP_THROW(RepoException( _("Can't figure out where the service is stored.") ));
+      ZYPP_THROW(ServiceException( oldService, _("Can't figure out where the service is stored.") ));
     }
 
     // remember: there may multiple services being defined in one file:
@@ -2050,25 +2366,34 @@ namespace zypp
 
     _services.erase(oldAlias);
     _services.insert(service);
+    // check for credentials in Urls
+    UrlCredentialExtractor( _options.rootDir ).collect( service.url() );
+
 
     // changed properties affecting also repositories
-    if( oldAlias != service.alias()                    // changed alias
-        || oldService.enabled() != service.enabled()   // changed enabled status
-      )
+    if ( oldAlias != service.alias()                   // changed alias
+      || oldService.enabled() != service.enabled() )   // changed enabled status
     {
       std::vector<RepoInfo> toModify;
       getRepositoriesInService(oldAlias, std::back_inserter(toModify));
       for_( it, toModify.begin(), toModify.end() )
       {
-        if (oldService.enabled() && !service.enabled())
-          it->setEnabled(false);
-        else if (!oldService.enabled() && service.enabled())
-        {
-          //! \todo do nothing? the repos will be enabled on service refresh
-          //! \todo how to know the service needs a (auto) refresh????
-        }
-        else
+       if ( oldService.enabled() != service.enabled() )
+       {
+         if ( service.enabled() )
+         {
+           // reset to last refreshs state
+           const auto & last = service.repoStates().find( it->alias() );
+           if ( last != service.repoStates().end() )
+             it->setEnabled( last->second.enabled );
+         }
+         else
+           it->setEnabled( false );
+       }
+
+        if ( oldAlias != service.alias() )
           it->setService(service.alias());
+
         modifyRepository(it->alias(), *it);
       }
     }
@@ -2253,14 +2578,14 @@ namespace zypp
   void RepoManager::removeService( const ServiceInfo & service )
   { return _pimpl->removeService( service ); }
 
-  void RepoManager::refreshServices()
-  { return _pimpl->refreshServices(); }
+  void RepoManager::refreshServices( const RefreshServiceOptions & options_r )
+  { return _pimpl->refreshServices( options_r ); }
 
-  void RepoManager::refreshService( const std::string & alias )
-  { return _pimpl->refreshService( alias ); }
+  void RepoManager::refreshService( const std::string & alias, const RefreshServiceOptions & options_r )
+  { return _pimpl->refreshService( alias, options_r ); }
 
-  void RepoManager::refreshService( const ServiceInfo & service )
-  { return _pimpl->refreshService( service ); }
+  void RepoManager::refreshService( const ServiceInfo & service, const RefreshServiceOptions & options_r )
+  { return _pimpl->refreshService( service, options_r ); }
 
   void RepoManager::modifyService( const std::string & oldAlias, const ServiceInfo & service )
   { return _pimpl->modifyService( oldAlias, service ); }