Imported Upstream version 17.0.0
[platform/upstream/libzypp.git] / zypp / RepoManager.cc
index aac27d1..50ddf46 100644 (file)
 #include <map>
 #include <algorithm>
 
+#include <solv/solvversion.h>
+
 #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"
@@ -41,7 +44,6 @@
 #include "zypp/repo/ServiceRepos.h"
 #include "zypp/repo/yum/Downloader.h"
 #include "zypp/repo/susetags/Downloader.h"
-#include "zypp/parser/plaindir/RepoParser.h"
 #include "zypp/repo/PluginServices.h"
 
 #include "zypp/Target.h" // for Target::targetDistribution() for repo index services
@@ -61,6 +63,89 @@ using namespace zypp::repo;
 ///////////////////////////////////////////////////////////////////
 namespace zypp
 {
+
+  ///////////////////////////////////////////////////////////////////
+  namespace env
+  {
+    /** To trigger appdata refresh unconditionally */
+    inline bool ZYPP_PLUGIN_APPDATA_FORCE_COLLECT()
+    {
+      const char * env = getenv("ZYPP_PLUGIN_APPDATA_FORCE_COLLECT");
+      return( env && str::strToBool( env, true ) );
+    }
+  } // namespace env
+  ///////////////////////////////////////////////////////////////////
+
+  ///////////////////////////////////////////////////////////////////
+  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
   {
@@ -149,7 +234,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
@@ -204,7 +289,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);
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -221,23 +306,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;
@@ -248,7 +345,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] == '.')
@@ -259,7 +356,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] == '.')
@@ -283,6 +380,15 @@ namespace zypp
 
     ////////////////////////////////////////////////////////////////////////////
 
+    ///////////////////////////////////////////////////////////////////
+    namespace
+    {
+      /** Whether repo is not under RM control and provides it's own methadata paths. */
+      inline bool isTmpRepo( const RepoInfo & info_r )
+      { return( info_r.filepath().empty() && info_r.usesAutoMethadataPaths() ); }
+    } // namespace
+    ///////////////////////////////////////////////////////////////////
+
     /**
      * \short Calculates the raw cache path for a repository, this is usually
      * /var/cache/zypp/alias
@@ -290,7 +396,7 @@ namespace zypp
     inline Pathname rawcache_path_for_repoinfo( const RepoManagerOptions &opt, const RepoInfo &info )
     {
       assert_alias(info);
-      return opt.repoRawCachePath / info.escaped_alias();
+      return isTmpRepo( info ) ? info.metadataPath() : opt.repoRawCachePath / info.escaped_alias();
     }
 
     /**
@@ -302,10 +408,7 @@ namespace zypp
      * for example /var/cache/zypp/alias/addondir
      */
     inline Pathname rawproductdata_path_for_repoinfo( const RepoManagerOptions &opt, const RepoInfo &info )
-    {
-      assert_alias(info);
-      return opt.repoRawCachePath / info.escaped_alias() / info.path();
-    }
+    { return rawcache_path_for_repoinfo( opt, info ) / info.path(); }
 
     /**
      * \short Calculates the packages cache path for a repository
@@ -313,16 +416,16 @@ namespace zypp
     inline Pathname packagescache_path_for_repoinfo( const RepoManagerOptions &opt, const RepoInfo &info )
     {
       assert_alias(info);
-      return opt.repoPackagesCachePath / info.escaped_alias();
+      return isTmpRepo( info ) ? info.packagesPath() : opt.repoPackagesCachePath / info.escaped_alias();
     }
 
     /**
      * \short Calculates the solv cache path for a repository
      */
-    inline Pathname solv_path_for_repoinfo( const RepoManagerOptions &opt, const RepoInfo &info)
+    inline Pathname solv_path_for_repoinfo( const RepoManagerOptions &opt, const RepoInfo &info )
     {
       assert_alias(info);
-      return opt.repoSolvCachePath / info.escaped_alias();
+      return isTmpRepo( info ) ? info.metadataPath().dirname() / "%SLV%" : opt.repoSolvCachePath / info.escaped_alias();
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -400,6 +503,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.
@@ -415,19 +533,63 @@ namespace zypp
       init_knownRepositories();
     }
 
+    ~Impl()
+    {
+      // trigger appdata refresh if some repos change
+      if ( ( _reposDirty || env::ZYPP_PLUGIN_APPDATA_FORCE_COLLECT() )
+       && 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:
@@ -450,6 +612,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 );
 
@@ -498,11 +661,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 );
 
@@ -532,8 +695,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);
     }
 
@@ -541,11 +704,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 */
@@ -631,6 +799,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;
@@ -638,37 +837,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 ( const 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;
   }
@@ -679,39 +901,26 @@ namespace zypp
   {
     Pathname mediarootpath = rawcache_path_for_repoinfo( _options, info );
     Pathname productdatapath = rawproductdata_path_for_repoinfo( _options, info );
-    RepoType repokind = info.type();
-    RepoStatus status;
 
-    switch ( repokind.toEnum() )
-    {
-      case RepoType::NONE_e:
-       // unknown, probe the local metadata
-       repokind = probe( productdatapath.asUrl() );
-      break;
-      default:
-      break;
-    }
+    RepoType repokind = info.type();
+    // If unknown, probe the local metadata
+    if ( repokind == RepoType::NONE )
+      repokind = probeCache( productdatapath );
 
+    RepoStatus status;
     switch ( repokind.toEnum() )
     {
       case RepoType::RPMMD_e :
-      {
-        status = RepoStatus( productdatapath + "/repodata/repomd.xml");
-      }
-      break;
+        status = RepoStatus( productdatapath/"repodata/repomd.xml") && RepoStatus( mediarootpath/"media.1/media" );
+       break;
 
       case RepoType::YAST2_e :
-      {
-        status = RepoStatus( productdatapath + "/content") && (RepoStatus( mediarootpath + "/media.1/media"));
-      }
-      break;
+        status = RepoStatus( productdatapath/"content" ) && RepoStatus( mediarootpath/"media.1/media" );
+       break;
 
       case RepoType::RPMPLAINDIR_e :
-      {
-        if ( PathInfo(Pathname(productdatapath + "/cookie")).isExist() )
-          status = RepoStatus( productdatapath + "/cookie");
-      }
-      break;
+       status = RepoStatus::fromCookieFile( productdatapath/"cookie" );
+       break;
 
       case RepoType::NONE_e :
        // Return default RepoStatus in case of RepoType::NONE
@@ -730,7 +939,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;
@@ -763,36 +972,39 @@ namespace zypp
   RepoManager::RefreshCheckStatus RepoManager::Impl::checkIfToRefreshMetadata( const RepoInfo & info, const Url & url, RawMetadataRefreshPolicy policy )
   {
     assert_alias(info);
-
-    RepoStatus oldstatus;
-    RepoStatus newstatus;
-
     try
     {
-      MIL << "Going to try to check whether refresh is needed for " << url << endl;
+      MIL << "Going to try to check whether refresh is needed for " << url << " (" << info.type() << ")" << endl;
 
       // first check old (cached) metadata
       Pathname mediarootpath = rawcache_path_for_repoinfo( _options, info );
-      filesystem::assert_dir(mediarootpath);
-      oldstatus = metadataStatus(info);
-
+      filesystem::assert_dir( mediarootpath );
+      RepoStatus oldstatus = metadataStatus( info );
       if ( oldstatus.empty() )
       {
         MIL << "No cached metadata, going to refresh" << endl;
         return REFRESH_NEEDED;
       }
 
+      if ( url.schemeIsVolatile() )
       {
-        std::string scheme( url.getScheme() );
-        if ( scheme == "cd" || scheme == "dvd" )
-        {
-          MIL << "never refresh CD/DVD" << endl;
-          return REPO_UP_TO_DATE;
-        }
+       MIL << "Never refresh CD/DVD" << endl;
+       return REPO_UP_TO_DATE;
+      }
+
+      if ( policy == RefreshForced )
+      {
+       MIL << "Forced refresh!" << endl;
+       return REFRESH_NEEDED;
+      }
+
+      if ( url.schemeIsLocal() )
+      {
+       policy = RefreshIfNeededIgnoreDelay;
       }
 
       // now we've got the old (cached) status, we can decide repo.refresh.delay
-      if (policy != RefreshForced && policy != RefreshIfNeededIgnoreDelay)
+      if ( policy != RefreshIfNeededIgnoreDelay )
       {
         // difference in seconds
         double diff = difftime(
@@ -820,82 +1032,50 @@ 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 the type is unknown, try probing.
-      switch ( repokind.toEnum() )
-      {
-        case RepoType::NONE_e:
-          // unknown, probe it \todo respect productdir
-          repokind = probe( url, info.path() );
-        break;
-        default:
-        break;
-      }
+      // if unknown: probe it
+      if ( repokind == RepoType::NONE )
+       repokind = probe( url, info.path() );
 
-      if ( ( repokind.toEnum() == RepoType::RPMMD_e ) ||
-           ( repokind.toEnum() == RepoType::YAST2_e ) )
+      // retrieve newstatus
+      RepoStatus newstatus;
+      switch ( repokind.toEnum() )
       {
-        MediaSetAccess media(url);
-        shared_ptr<repo::Downloader> downloader_ptr;
-
-        if ( repokind.toEnum() == RepoType::RPMMD_e )
-          downloader_ptr.reset(new yum::Downloader(info, mediarootpath));
-        else
-          downloader_ptr.reset( new susetags::Downloader(info, mediarootpath));
+       case RepoType::RPMMD_e:
+       {
+         MediaSetAccess media( url );
+         newstatus = yum::Downloader( info, mediarootpath ).status( media );
+       }
+       break;
 
-        RepoStatus newstatus = downloader_ptr->status(media);
-        bool refresh = false;
-        if ( oldstatus.checksum() == newstatus.checksum() )
-        {
-          MIL << "repo has not changed" << endl;
-          if ( policy == RefreshForced )
-          {
-            MIL << "refresh set to forced" << endl;
-            refresh = true;
-          }
-        }
-        else
-        {
-          MIL << "repo has changed, going to refresh" << endl;
-          refresh = true;
-        }
+       case RepoType::YAST2_e:
+       {
+         MediaSetAccess media( url );
+         newstatus = susetags::Downloader( info, mediarootpath ).status( media );
+       }
+       break;
 
-        if (!refresh)
-          touchIndexFile(info);
+       case RepoType::RPMPLAINDIR_e:
+         newstatus = RepoStatus( MediaMounter(url).getPathName(info.path()) ); // dir status
+         break;
 
-        return refresh ? REFRESH_NEEDED : REPO_UP_TO_DATE;
+       default:
+       case RepoType::NONE_e:
+         ZYPP_THROW( RepoUnknownTypeException( info ) );
+         break;
       }
-      else if ( repokind.toEnum() == RepoType::RPMPLAINDIR_e )
-      {
-        MediaMounter media( url );
-        RepoStatus newstatus = parser::plaindir::dirStatus( media.getPathName( info.path() ) );
-        bool refresh = false;
-        if ( oldstatus.checksum() == newstatus.checksum() )
-        {
-          MIL << "repo has not changed" << endl;
-          if ( policy == RefreshForced )
-          {
-            MIL << "refresh set to forced" << endl;
-            refresh = true;
-          }
-        }
-        else
-        {
-          MIL << "repo has changed, going to refresh" << endl;
-          refresh = true;
-        }
 
-        if (!refresh)
-          touchIndexFile(info);
-
-        return refresh ? REFRESH_NEEDED : REPO_UP_TO_DATE;
+      // check status
+      if ( oldstatus == newstatus )
+      {
+       MIL << "repo has not changed" << endl;
+       touchIndexFile( info );
+       return REPO_UP_TO_DATE;
       }
-      else
+      else // includes newstatus.empty() if e.g. repo format changed
       {
-        ZYPP_THROW(RepoUnknownTypeException(info));
+       MIL << "repo has changed, going to refresh" << endl;
+       return REFRESH_NEEDED;
       }
     }
     catch ( const Exception &e )
@@ -915,10 +1095,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 )
     {
@@ -933,35 +1115,30 @@ namespace zypp
 
         MIL << "Going to refresh metadata from " << url << endl;
 
+       // bsc#1048315: Always re-probe in case of repo format change.
+       // TODO: Would be sufficient to verify the type and re-probe
+       // if verification failed (or type is RepoType::NONE)
         repo::RepoType repokind = info.type();
-
-        // if the type is unknown, try probing.
-        switch ( repokind.toEnum() )
-        {
-          case RepoType::NONE_e:
-            // unknown, probe it
-            repokind = probe( *it, info.path() );
-
-            if (repokind.toEnum() != RepoType::NONE_e)
-            {
-              // Adjust the probed type in RepoInfo
-              info.setProbedType( repokind ); // lazy init!
-              //save probed type only for repos in system
-              for_( it, repoBegin(), repoEnd() )
-              {
-                if ( info.alias() == (*it).alias() )
-                {
-                  RepoInfo modifiedrepo = info;
-                  modifiedrepo.setType( repokind );
-                  modifyRepository( info.alias(), modifiedrepo );
-                  break;
-                }
-              }
-            }
-          break;
-          default:
-          break;
-        }
+       {
+         repo::RepoType probed = probe( *it, info.path() );
+         if ( repokind != probed )
+         {
+           repokind = probed;
+           // Adjust the probed type in RepoInfo
+           info.setProbedType( repokind ); // lazy init!
+           //save probed type only for repos in system
+           for_( it, repoBegin(), repoEnd() )
+           {
+             if ( info.alias() == (*it).alias() )
+             {
+               RepoInfo modifiedrepo = info;
+               modifiedrepo.setType( repokind );
+               modifyRepository( info.alias(), modifiedrepo );
+               break;
+             }
+           }
+         }
+       }
 
         Pathname mediarootpath = rawcache_path_for_repoinfo( _options, info );
         if( filesystem::assert_dir(mediarootpath) )
@@ -1009,32 +1186,22 @@ namespace zypp
         else if ( repokind.toEnum() == RepoType::RPMPLAINDIR_e )
         {
           MediaMounter media( url );
-          RepoStatus newstatus = parser::plaindir::dirStatus( media.getPathName( info.path() ) );
+          RepoStatus newstatus = RepoStatus( media.getPathName( info.path() ) );       // dir status
 
           Pathname productpath( tmpdir.path() / info.path() );
           filesystem::assert_dir( productpath );
-          std::ofstream file( (productpath/"cookie").c_str() );
-          if ( !file )
-          {
-            // TranslatorExplanation '%s' is a filename
-            ZYPP_THROW( Exception(str::form( _("Can't open file '%s' for writing."), (productpath/"cookie").c_str() )));
-          }
-          file << url;
-          if ( ! info.path().empty() && info.path() != "/" )
-            file << " (" << info.path() << ")";
-          file << endl;
-          file << newstatus.checksum() << endl;
-
-          file.close();
+         newstatus.saveToCookieFile( productpath/"cookie" );
         }
         else
         {
-          ZYPP_THROW(RepoUnknownTypeException());
+          ZYPP_THROW(RepoUnknownTypeException( info ));
         }
 
         // ok we have the metadata, now exchange
         // the contents
        filesystem::exchange( tmpdir.path(), mediarootpath );
+       if ( ! isTmpRepo( info ) )
+         reposManip(); // remember to trigger appdata refresh
 
         // we are done.
         return;
@@ -1049,6 +1216,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,11 +1274,17 @@ namespace zypp
       MIL << info.alias() << " is already cached." << endl;
       RepoStatus cache_status = cacheStatus(info);
 
-      if ( cache_status.checksum() == raw_metadata_status.checksum() )
+      if ( cache_status == raw_metadata_status )
       {
         MIL << info.alias() << " cache is up to date with metadata." << endl;
-        if ( policy == BuildIfNeeded ) {
-          return;
+        if ( policy == BuildIfNeeded )
+       {
+         // On the fly add missing solv.idx files for bash completion.
+         const Pathname & base = solv_path_for_repoinfo( _options, info);
+         if ( ! PathInfo(base/"solv.idx").isExist() )
+           sat::updateSolvFileIndex( base/"solv" );
+
+         return;
         }
         else {
           MIL << info.alias() << " cache rebuild is forced" << endl;
@@ -1154,7 +1330,7 @@ namespace zypp
     {
       case RepoType::NONE_e:
         // unknown, probe the local metadata
-        repokind = probe( productdatapath.asUrl() );
+        repokind = probeCache( productdatapath );
       break;
       default:
       break;
@@ -1173,7 +1349,7 @@ namespace zypp
         scoped_ptr<MediaMounter> forPlainDirs;
 
         ExternalProgram::Arguments cmd;
-        cmd.push_back( "repo2solv.sh" );
+        cmd.push_back( PathInfo( "/usr/bin/repo2solv" ).isFile() ? "repo2solv" : "repo2solv.sh" );
         // repo2solv expects -o as 1st arg!
         cmd.push_back( "-o" );
         cmd.push_back( solvfile.asString() );
@@ -1181,7 +1357,7 @@ namespace zypp
 
         if ( repokind == RepoType::RPMPLAINDIR )
         {
-          forPlainDirs.reset( new MediaMounter( *info.baseUrlsBegin() ) );
+          forPlainDirs.reset( new MediaMounter( info.url() ) );
           // recusive for plaindir as 2nd arg!
           cmd.push_back( "-R" );
           // FIXME this does only work form dir: URLs
@@ -1212,10 +1388,11 @@ namespace zypp
 
         // We keep it.
         guard.resetDispose();
+       sat::updateSolvFileIndex( solvfile );   // content digest for zypper bash completion
       }
       break;
       default:
-        ZYPP_THROW(RepoUnknownTypeException( _("Unhandled repository type") ));
+        ZYPP_THROW(RepoUnknownTypeException( info, _("Unhandled repository type") ));
       break;
     }
     // update timestamp and checksum
@@ -1226,6 +1403,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;
@@ -1310,6 +1494,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 )
@@ -1344,7 +1550,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 );
@@ -1386,19 +1592,13 @@ namespace zypp
     {
       Repository repo = sat::Pool::instance().addRepoSolv( solvfile, info );
       // test toolversion in order to rebuild solv file in case
-      // it was written by an old libsolv-tool parser.
-      //
-      // Known version strings used:
-      //  - <no string>
-      //  - "1.0"
-      //
-      sat::LookupRepoAttr toolversion( sat::SolvAttr::repositoryToolVersion, repo );
-      if ( toolversion.begin().asString().empty() )
+      // it was written by a different libsolv-tool parser.
+      const std::string & toolversion( sat::LookupRepoAttr( sat::SolvAttr::repositoryToolVersion, repo ).begin().asString() );
+      if ( toolversion != LIBSOLV_TOOLVERSION )
       {
         repo.eraseFromPool();
-        ZYPP_THROW(Exception("Solv-file was created by old parser."));
+        ZYPP_THROW(Exception(str::Str() << "Solv-file was created by '"<<toolversion<<"'-parser (want "<<LIBSOLV_TOOLVERSION<<")."));
       }
-      // else: up-to-date (or even newer).
     }
     catch ( const Exception & exp )
     {
@@ -1426,23 +1626,20 @@ 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
     if ( _options.probe )
     {
       DBG << "unknown repository type, probing" << endl;
+      assert_urls(tosave);
 
-      RepoType probedtype;
-      probedtype = probe( *tosave.baseUrlsBegin(), info.path() );
-      if ( tosave.baseUrlsSize() > 0 )
-      {
-        if ( probedtype == RepoType::NONE )
-          ZYPP_THROW(RepoUnknownTypeException());
-        else
-          tosave.setType(probedtype);
-      }
+      RepoType probedtype( probe( tosave.url(), info.path() ) );
+      if ( probedtype == RepoType::NONE )
+       ZYPP_THROW(RepoUnknownTypeException(info));
+      else
+       tosave.setType(probedtype);
     }
 
     progress.set(50);
@@ -1464,40 +1661,24 @@ namespace zypp
 
     tosave.dumpAsIniOn(file);
     tosave.setFilepath(repofile);
-    tosave.setMetadataPath( metadataPath( tosave ) );
-    tosave.setPackagesPath( packagesPath( tosave ) );
+    tosave.setMetadataPath( rawcache_path_for_repoinfo( _options, tosave ) );
+    tosave.setPackagesPath( packagescache_path_for_repoinfo( _options, tosave ) );
     {
-      // We chould fix the API as we must injet those paths
+      // We should fix the API as we must inject those paths
       // into the repoinfo in order to keep it usable.
       RepoInfo & oinfo( const_cast<RepoInfo &>(info) );
-      oinfo.setMetadataPath( metadataPath( tosave ) );
-      oinfo.setPackagesPath( packagesPath( tosave ) );
+      oinfo.setFilepath(repofile);
+      oinfo.setMetadataPath( rawcache_path_for_repoinfo( _options, tosave ) );
+      oinfo.setPackagesPath( packagescache_path_for_repoinfo( _options, 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) );
-
-      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));
-    }
+    UrlCredentialExtractor( _options.rootDir ).collect( tosave.baseUrls() );
 
-    HistoryLog().addRepository(tosave);
+    HistoryLog(_options.rootDir).addRepository(tosave);
 
     progress.toMax();
     MIL << "done" << endl;
@@ -1549,9 +1730,11 @@ namespace zypp
           ++it )
     {
       MIL << "Saving " << (*it).alias() << endl;
-      it->setFilepath(repofile.asString());
       it->dumpAsIniOn(file);
-      _repos.insert(*it);
+      it->setFilepath(repofile);
+      it->setMetadataPath( rawcache_path_for_repoinfo( _options, *it ) );
+      it->setPackagesPath( packagescache_path_for_repoinfo( _options, *it ) );
+      reposManip().insert(*it);
 
       HistoryLog(_options.rootDir).addRepository(*it);
     }
@@ -1585,21 +1768,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
         {
@@ -1635,8 +1820,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
@@ -1661,7 +1846,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
     {
@@ -1694,9 +1879,29 @@ namespace zypp
             newinfo.dumpAsIniOn(file);
       }
 
+      if ( toedit.enabled() && !newinfo.enabled() )
+      {
+       // On the fly remove solv.idx files for bash completion if a repo gets disabled.
+       const Pathname & solvidx = solv_path_for_repoinfo(_options, newinfo)/"solv.idx";
+       if ( PathInfo(solvidx).isExist() )
+         filesystem::unlink( solvidx );
+      }
+
       newinfo.setFilepath(toedit.filepath());
-      _repos.erase(toedit);
-      _repos.insert(newinfo);
+      newinfo.setMetadataPath( rawcache_path_for_repoinfo( _options, newinfo ) );
+      newinfo.setPackagesPath( packagescache_path_for_repoinfo( _options, newinfo ) );
+      {
+       // We should fix the API as we must inject those paths
+       // into the repoinfo in order to keep it usable.
+       RepoInfo & oinfo( const_cast<RepoInfo &>(newinfo_r) );
+       oinfo.setFilepath(toedit.filepath());
+       oinfo.setMetadataPath( rawcache_path_for_repoinfo( _options, newinfo ) );
+       oinfo.setPackagesPath( packagescache_path_for_repoinfo( _options, 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;
     }
@@ -1706,8 +1911,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 );
@@ -1750,15 +1955,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;
   }
@@ -1767,14 +1965,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;
@@ -1786,9 +1984,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
     {
@@ -1807,7 +2005,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
@@ -1821,7 +2019,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
@@ -1832,25 +2030,46 @@ 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 );
     assert_url( service );
+    MIL << "Going to refresh service '" << service.alias() <<  "', url: " << service.url() << ", opts: " << options_r << endl;
+
+    if ( service.ttl() && !( options_r.testFlag( RefreshService_forceRefresh) || options_r.testFlag( RefreshService_restoreStatus ) ) )
+    {
+      // Service defines a TTL; maybe we can re-use existing data without refresh.
+      Date lrf = service.lrf();
+      if ( lrf )
+      {
+       Date now( Date::now() );
+       if ( lrf <= now )
+       {
+         if ( (lrf+=service.ttl()) > now ) // lrf+= !
+         {
+           MIL << "Skip: '" << service.alias() << "' metadata valid until " << lrf << endl;
+           return;
+         }
+       }
+       else
+         WAR << "Force: '" << service.alias() << "' metadata last refresh in the future: " << lrf << endl;
+      }
+    }
+
     // NOTE: It might be necessary to modify and rewrite the service info.
     // 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;
 
-    //! \todo add callbacks for apps (start, end, repo removed, repo added, repo changed)
+    //! \todo add callbacks for apps (start, end, repo removed, repo added, repo changed)?
 
     // if the type is unknown, try probing.
     if ( service.type() == repo::ServiceType::NONE )
@@ -1872,6 +2091,7 @@ namespace zypp
     DBG << "ServicesTargetDistro: " << servicesTargetDistro << endl;
 
     // parse it
+    Date::Duration origTtl = service.ttl();    // FIXME Ugly hack: const service.ttl modified when parsing
     RepoCollector collector(servicesTargetDistro);
     // FIXME Ugly hack: ServiceRepos may throw ServicePluginInformalException
     // which is actually a notification. Using an exception for this
@@ -1879,7 +2099,7 @@ namespace zypp
     // and in zypper.
     std::pair<DefaultIntegral<bool,false>, repo::ServicePluginInformalException> uglyHack;
     try {
-      ServiceRepos repos(service, bind( &RepoCollector::collect, &collector, _1 ));
+      ServiceRepos( service, bind( &RepoCollector::collect, &collector, _1 ) );
     }
     catch ( const repo::ServicePluginInformalException & e )
     {
@@ -1887,38 +2107,57 @@ namespace zypp
       uglyHack.first = true;
       uglyHack.second = e;
     }
+    if ( service.ttl() != origTtl )    // repoindex.xml changed ttl
+    {
+      if ( !service.ttl() )
+       service.setLrf( Date() );       // don't need lrf when zero ttl
+      serviceModified = true;
+    }
+    ////////////////////////////////////////////////////////////////////////////
+    // 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) );
+      }
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -1927,47 +2166,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 ) );
@@ -1975,60 +2238,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 )
@@ -2045,12 +2366,28 @@ 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 ( service.type() != ServiceType::PLUGIN )
     {
-      // write out modified service file.
-      modifyService( service.alias(), service );
+      if ( service.ttl() )
+      {
+       service.setLrf( Date::now() );  // remember last refresh
+       serviceModified =  true;        // or use a cookie file
+      }
+
+      if ( serviceModified )
+      {
+       // write out modified service file.
+       modifyService( service.alias(), service );
+      }
     }
 
     if ( uglyHack.first )
@@ -2071,8 +2408,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);
@@ -2080,7 +2416,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:
@@ -2100,25 +2436,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);
       }
     }
@@ -2303,14 +2648,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 ); }