chromium/chromeos/printing/printer_config_cache.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/printing/printer_config_cache.h"

#include <memory>
#include <optional>
#include <string_view>
#include <utility>
#include <vector>

#include "base/containers/queue.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "url/gurl.h"

namespace chromeos {

namespace {

// Defines the serving root in which all PPDs and PPD metadata reside.
constexpr char kServingRoot[] =
    "https://printerconfigurations.googleusercontent.com/"
    "chromeos_printing/";
constexpr char kLocalhostRoot[] = "http://localhost:7002/";

// Prepends the serving root to |name|, returning the result.
std::string PrependServingRoot(const std::string& name,
                               bool use_localhost_as_root) {
  if (use_localhost_as_root) {
    return base::StrCat({kLocalhostRoot, name});
  }
  return base::StrCat({kServingRoot, name});
}

// Accepts a relative |path| to a value in the Chrome OS Printing
// serving root) and returns a resource request to satisfy the same.
std::unique_ptr<network::ResourceRequest> FormRequest(
    const std::string& path,
    bool use_localhost_as_root) {
  GURL full_url(PrependServingRoot(path, use_localhost_as_root));
  if (!full_url.is_valid()) {
    return nullptr;
  }

  auto request = std::make_unique<network::ResourceRequest>();
  request->url = full_url;

  request->load_flags = net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
  request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  return request;
}

}  // namespace

// In case of fetch failure, only the key is meaningful feedback.
// static
PrinterConfigCache::FetchResult PrinterConfigCache::FetchResult::Failure(
    const std::string& key) {
  return PrinterConfigCache::FetchResult{false, key, std::string(),
                                         base::Time()};
}

// static
PrinterConfigCache::FetchResult PrinterConfigCache::FetchResult::Success(
    const std::string& key,
    const std::string& contents,
    base::Time time_of_fetch) {
  return PrinterConfigCache::FetchResult{true, key, contents, time_of_fetch};
}

class PrinterConfigCacheImpl : public PrinterConfigCache {
 public:
  explicit PrinterConfigCacheImpl(
      const base::Clock* clock,
      base::RepeatingCallback<network::mojom::URLLoaderFactory*()>
          loader_factory_dispenser,
      bool use_localhost_as_root)
      : clock_(clock),
        loader_factory_dispenser_(std::move(loader_factory_dispenser)),
        use_localhost_as_root_(use_localhost_as_root),
        weak_factory_(this) {}

  ~PrinterConfigCacheImpl() override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  }

  void Fetch(const std::string& key,
             base::TimeDelta expiration,
             FetchCallback cb) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    // Try to answer this fetch request locally.
    const auto& finding = cache_.find(key);
    if (finding != cache_.end()) {
      const Entry& entry = finding->second;
      if (entry.time_of_fetch + expiration > clock_->Now()) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE, base::BindOnce(std::move(cb), FetchResult::Success(
                                                         key, entry.contents,
                                                         entry.time_of_fetch)));
        return;
      }
    }

    // We couldn't answer this request locally. Issue a networked fetch
    // and defer the answer to when we hear back.
    auto context = std::make_unique<FetchContext>(key, std::move(cb));
    fetch_queue_.push(std::move(context));
    TryToStartNetworkedFetch();
  }

  void Drop(const std::string& key) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    cache_.erase(key);
  }

 private:
  // A FetchContext saves off the key and the FetchCallback that a
  // caller passes to PrinterConfigCacheImpl::Fetch().
  struct FetchContext {
    const std::string key;
    PrinterConfigCache::FetchCallback cb;

    FetchContext(const std::string& arg_key,
                 PrinterConfigCache::FetchCallback arg_cb)
        : key(arg_key), cb(std::move(arg_cb)) {}
    ~FetchContext() = default;
  };

  // If a PrinterConfigCache maps keys to values, then Entry structs
  // represent values.
  struct Entry {
    std::string contents;
    base::Time time_of_fetch;

    Entry(const std::string& arg_contents, base::Time time)
        : contents(arg_contents), time_of_fetch(time) {}
    ~Entry() = default;
  };

  void TryToStartNetworkedFetch() {
    // Either
    // 1. a networked fetch is already in flight or
    // 2. there are no more pending networked fetches to act upon.
    // In either case, we can't do anything at the moment; back off
    // and let a future call to Fetch() or FinishNetworkedFetch()
    // return here to try again.
    if (fetcher_ || fetch_queue_.empty()) {
      return;
    }

    std::unique_ptr<FetchContext> context = std::move(fetch_queue_.front());
    fetch_queue_.pop();
    auto request = FormRequest(context->key, use_localhost_as_root_);

    // Create traffic annotation tag.
    net::NetworkTrafficAnnotationTag traffic_annotation =
        net::DefineNetworkTrafficAnnotation("printer_config_fetch", R"(
          semantics {
            sender: "Printer Configuration"
            description:
              "This component sends requests to the Chrome OS Printing "
              "serving root during printer configuration. This can return "
              "two pieces of information, depending on the request: "
              "PostScript Printer Description (PPD) files for a specified "
              "printer, and PPD file metadata to help locate the desired PPD "
              "file."
            trigger: "On printer setup in ChromeOS."
            data: "Printer names (comprising of make and/or model)."
            user_data: {
              type: OTHER
            }
            destination: GOOGLE_OWNED_SERVICE
            internal: {
              contacts: {
                email: "[email protected]"
              }
            }
            last_reviewed: "2023-01-18"
          }
          policy {
            cookies_allowed: NO
            setting:
              "Admins must disable access to both enterprise and "
              "non-enterprise printers. Enterprise printers should be left "
              "empty under 'Devices > Chrome > Printers'. Non-enterprise "
              "printers can be disabled under 'Devices > Chrome > Settings > "
              "Printer management' by setting to: 'Do not allow users to add "
              "new printers'."
            chrome_policy {
              UserPrintersAllowed {
                UserPrintersAllowed: false
              }
              PrintersBulkConfiguration: {
                PrintersBulkConfiguration: ""
              }
            }
            chrome_device_policy {
              # DevicePrinters
              device_printers: {
                external_policy: ""
              }
            }
          })");
    fetcher_ = network::SimpleURLLoader::Create(std::move(request),
                                                traffic_annotation);

    fetcher_->DownloadToString(
        loader_factory_dispenser_.Run(),
        base::BindOnce(&PrinterConfigCacheImpl::FinishNetworkedFetch,
                       weak_factory_.GetWeakPtr(), std::move(context)),
        network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
  }

  // Called by |fetcher_| once DownloadToString() completes.
  void FinishNetworkedFetch(std::unique_ptr<FetchContext> context,
                            std::unique_ptr<std::string> contents) {
    // Wherever |fetcher_| works its sorcery, it had better have posted
    // back onto _our_ sequence.
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    if (fetcher_->NetError() == net::Error::OK) {
      // We only want to update our local cache if the |fetcher_|
      // succeeded; otherwise, prefer to either retain the stale entry
      // (if extant) or retain no entry at all (if not).
      const Entry newly_inserted = Entry(*contents, clock_->Now());
      cache_.insert_or_assign(context->key, newly_inserted);
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(context->cb),
                                    FetchResult::Success(
                                        context->key, newly_inserted.contents,
                                        newly_inserted.time_of_fetch)));
    } else {
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(context->cb),
                                    FetchResult::Failure(context->key)));
    }

    fetcher_.reset();
    TryToStartNetworkedFetch();
  }

  // The heart of an PrinterConfigCache: the local cache itself.
  base::flat_map<std::string, Entry> cache_;

  // Enqueues networked requests.
  base::queue<std::unique_ptr<FetchContext>> fetch_queue_;

  // Dispenses Time objects to mark time of fetch on Entry instances.
  raw_ptr<const base::Clock> clock_;

  // Dispenses fresh URLLoaderFactory instances; see header comment
  // on Create().
  base::RepeatingCallback<network::mojom::URLLoaderFactory*()>
      loader_factory_dispenser_;

  // Talks to the networked service to fetch resources.
  //
  // Because this class is sequenced, a non-nullptr value here (observed
  // on-sequence) denotes an ongoing fetch. See the
  // TryToStartNetworkedFetch() and FinishNetworkedFetch() methods.
  std::unique_ptr<network::SimpleURLLoader> fetcher_;

  // Determines the address of the server.
  const bool use_localhost_as_root_;

  SEQUENCE_CHECKER(sequence_checker_);

  // Dispenses weak pointers to our |fetcher_|. This is necessary
  // because |this| could be deleted while the loader is in flight
  // off-sequence.
  base::WeakPtrFactory<PrinterConfigCacheImpl> weak_factory_;
};

// static
std::unique_ptr<PrinterConfigCache> PrinterConfigCache::Create(
    const base::Clock* clock,
    base::RepeatingCallback<network::mojom::URLLoaderFactory*()>
        loader_factory_dispenser,
    bool use_localhost_as_root) {
  return std::make_unique<PrinterConfigCacheImpl>(
      clock, std::move(loader_factory_dispenser), use_localhost_as_root);
}

}  // namespace chromeos