1010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)// Copyright 2014 The Chromium Authors. All rights reserved.
2010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
3010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)// found in the LICENSE file.
4010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
5010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "content/browser/service_worker/service_worker_read_from_cache_job.h"
6010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
75f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)#include <string>
85f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)#include <vector>
95f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
101320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci#include "base/debug/trace_event.h"
11010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "content/browser/service_worker/service_worker_context_core.h"
12010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "content/browser/service_worker/service_worker_disk_cache.h"
13116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch#include "content/browser/service_worker/service_worker_metrics.h"
14010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "net/base/io_buffer.h"
15010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "net/base/net_errors.h"
16010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "net/http/http_request_headers.h"
17010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "net/http/http_response_headers.h"
18010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "net/http/http_util.h"
19010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "net/url_request/url_request.h"
20010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)#include "net/url_request/url_request_status.h"
21010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
22010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)namespace content {
23010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
24010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)ServiceWorkerReadFromCacheJob::ServiceWorkerReadFromCacheJob(
25010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    net::URLRequest* request,
26010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    net::NetworkDelegate* network_delegate,
27010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    base::WeakPtr<ServiceWorkerContextCore> context,
28010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    int64 response_id)
29010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    : net::URLRequestJob(request, network_delegate),
30010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)      context_(context),
31010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)      response_id_(response_id),
32010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)      has_been_killed_(false),
33010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)      weak_factory_(this) {
34010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
35010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
36010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)ServiceWorkerReadFromCacheJob::~ServiceWorkerReadFromCacheJob() {
37010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
38010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
39010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)void ServiceWorkerReadFromCacheJob::Start() {
401320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  TRACE_EVENT_ASYNC_BEGIN1("ServiceWorker",
411320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                           "ServiceWorkerReadFromCacheJob::ReadInfo",
421320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                           this,
431320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                           "URL", request_->url().spec());
44010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (!context_) {
45010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    NotifyStartError(net::URLRequestStatus(
46010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)        net::URLRequestStatus::FAILED, net::ERR_FAILED));
47010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return;
48010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  }
49010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
50010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  // Create a response reader and start reading the headers,
51010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  // we'll continue when thats done.
52010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  reader_ = context_->storage()->CreateResponseReader(response_id_);
53010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  http_info_io_buffer_ = new HttpResponseInfoIOBuffer;
54010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  reader_->ReadInfo(
551320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci      http_info_io_buffer_.get(),
56010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)      base::Bind(&ServiceWorkerReadFromCacheJob::OnReadInfoComplete,
57010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)                 weak_factory_.GetWeakPtr()));
58010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  SetStatus(net::URLRequestStatus(net::URLRequestStatus::IO_PENDING, 0));
59010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
60010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
61010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)void ServiceWorkerReadFromCacheJob::Kill() {
62010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (has_been_killed_)
63010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return;
64010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  weak_factory_.InvalidateWeakPtrs();
65010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  has_been_killed_ = true;
66010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  reader_.reset();
67010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  context_.reset();
68010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  http_info_io_buffer_ = NULL;
69010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  http_info_.reset();
70010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  range_response_info_.reset();
71010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  net::URLRequestJob::Kill();
72010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
73010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
74010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)net::LoadState ServiceWorkerReadFromCacheJob::GetLoadState() const {
75cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  if (reader_.get() && reader_->IsReadPending())
76cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    return net::LOAD_STATE_READING_RESPONSE;
77cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  return net::LOAD_STATE_IDLE;
78010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
79010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
80010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)bool ServiceWorkerReadFromCacheJob::GetCharset(std::string* charset) {
81010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (!http_info())
82010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return false;
83010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  return http_info()->headers->GetCharset(charset);
84010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
85010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
86010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)bool ServiceWorkerReadFromCacheJob::GetMimeType(std::string* mime_type) const {
87010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (!http_info())
88010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return false;
89010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  return http_info()->headers->GetMimeType(mime_type);
90010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
91010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
92010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)void ServiceWorkerReadFromCacheJob::GetResponseInfo(
93010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    net::HttpResponseInfo* info) {
94010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (!http_info())
95010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return;
96010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  *info = *http_info();
97010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
98010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
99010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)int ServiceWorkerReadFromCacheJob::GetResponseCode() const {
100010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (!http_info())
101010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return -1;
102010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  return http_info()->headers->response_code();
103010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
104010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
105010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)void ServiceWorkerReadFromCacheJob::SetExtraRequestHeaders(
106010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)      const net::HttpRequestHeaders& headers) {
107010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  std::string value;
108010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  std::vector<net::HttpByteRange> ranges;
109010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (!headers.GetHeader(net::HttpRequestHeaders::kRange, &value) ||
110010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)      !net::HttpUtil::ParseRangeHeader(value, &ranges)) {
111010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return;
112010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  }
113010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
114010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  // If multiple ranges are requested, we play dumb and
115010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  // return the entire response with 200 OK.
116010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (ranges.size() == 1U)
117010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    range_requested_ = ranges[0];
118010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
119010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
120010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)bool ServiceWorkerReadFromCacheJob::ReadRawData(
121010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    net::IOBuffer* buf,
122010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    int buf_size,
123010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    int *bytes_read) {
124010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  DCHECK_NE(buf_size, 0);
125010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  DCHECK(bytes_read);
126010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  DCHECK(!reader_->IsReadPending());
1271320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  TRACE_EVENT_ASYNC_BEGIN1("ServiceWorker",
1281320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                           "ServiceWorkerReadFromCacheJob::ReadRawData",
1291320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                           this,
1301320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                           "URL", request_->url().spec());
131010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  reader_->ReadData(
132010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)      buf, buf_size, base::Bind(&ServiceWorkerReadFromCacheJob::OnReadComplete,
133010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)                                weak_factory_.GetWeakPtr()));
134010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  SetStatus(net::URLRequestStatus(net::URLRequestStatus::IO_PENDING, 0));
135010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  return false;
136010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
137010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
138010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)const net::HttpResponseInfo* ServiceWorkerReadFromCacheJob::http_info() const {
139010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (!http_info_)
140010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return NULL;
141010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (range_response_info_)
142010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return range_response_info_.get();
143010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  return http_info_.get();
144010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
145010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
146010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)void ServiceWorkerReadFromCacheJob::OnReadInfoComplete(int result) {
147010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  scoped_refptr<ServiceWorkerReadFromCacheJob> protect(this);
148010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (!http_info_io_buffer_->http_info) {
1495f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    DCHECK_LT(result, 0);
150116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    ServiceWorkerMetrics::CountReadResponseResult(
151116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        ServiceWorkerMetrics::READ_HEADERS_ERROR);
152010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    NotifyDone(net::URLRequestStatus(net::URLRequestStatus::FAILED, result));
153010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return;
154010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  }
1555f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  DCHECK_GE(result, 0);
156010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  SetStatus(net::URLRequestStatus());  // Clear the IO_PENDING status
157010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  http_info_.reset(http_info_io_buffer_->http_info.release());
158010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (is_range_request())
159010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    SetupRangeResponse(http_info_io_buffer_->response_data_size);
160010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  http_info_io_buffer_ = NULL;
1611320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  TRACE_EVENT_ASYNC_END1("ServiceWorker",
1621320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                         "ServiceWorkerReadFromCacheJob::ReadInfo",
1631320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                         this,
1641320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                         "Result", result);
165010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  NotifyHeadersComplete();
166010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
167010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
168010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)void ServiceWorkerReadFromCacheJob::SetupRangeResponse(int resource_size) {
169010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  DCHECK(is_range_request() && http_info_.get() && reader_.get());
170010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  if (resource_size < 0 || !range_requested_.ComputeBounds(resource_size)) {
171010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    range_requested_ = net::HttpByteRange();
172010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    return;
173010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  }
174010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
175010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  DCHECK(range_requested_.IsValid());
176010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  int offset = static_cast<int>(range_requested_.first_byte_position());
177010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  int length = static_cast<int>(range_requested_.last_byte_position() -
178010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)                                range_requested_.first_byte_position() + 1);
179010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
180010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  // Tell the reader about the range to read.
181010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  reader_->SetReadRange(offset, length);
182010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
183010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  // Make a copy of the full response headers and fix them up
184010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  // for the range we'll be returning.
185010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  range_response_info_.reset(new net::HttpResponseInfo(*http_info_));
186010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  net::HttpResponseHeaders* headers = range_response_info_->headers.get();
187010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  headers->UpdateWithNewRange(
188010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)      range_requested_, resource_size, true /* replace status line */);
189010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
190010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
191010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)void ServiceWorkerReadFromCacheJob::OnReadComplete(int result) {
192116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  ServiceWorkerMetrics::ReadResponseResult check_result;
193f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  if (result == 0) {
194116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    check_result = ServiceWorkerMetrics::READ_OK;
195010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    NotifyDone(net::URLRequestStatus());
196f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  } else if (result < 0) {
197116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    check_result = ServiceWorkerMetrics::READ_DATA_ERROR;
198010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    NotifyDone(net::URLRequestStatus(net::URLRequestStatus::FAILED, result));
199f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  } else {
200116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    check_result = ServiceWorkerMetrics::READ_OK;
201010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)    SetStatus(net::URLRequestStatus());  // Clear the IO_PENDING status
202f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  }
203116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  ServiceWorkerMetrics::CountReadResponseResult(check_result);
204010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)  NotifyReadComplete(result);
2051320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci  TRACE_EVENT_ASYNC_END1("ServiceWorker",
2061320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                         "ServiceWorkerReadFromCacheJob::ReadRawData",
2071320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                         this,
2081320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci                         "Result", result);
209010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}
210010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)
211010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)}  // namespace content
212