| OLD | NEW |
| 1 // Copyright (c) 2017 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "content/browser/appcache/appcache_url_loader_job.h" | 5 #include "content/browser/appcache/appcache_url_loader_job.h" |
| 6 | 6 |
| 7 #include "base/strings/string_number_conversions.h" | 7 #include "base/strings/string_number_conversions.h" |
| 8 #include "content/browser/appcache/appcache_histograms.h" | 8 #include "content/browser/appcache/appcache_histograms.h" |
| 9 #include "content/browser/appcache/appcache_subresource_url_factory.h" | 9 #include "content/browser/appcache/appcache_subresource_url_factory.h" |
| 10 #include "content/browser/appcache/appcache_url_loader_request.h" |
| 10 #include "content/browser/url_loader_factory_getter.h" | 11 #include "content/browser/url_loader_factory_getter.h" |
| 11 #include "content/common/net_adapters.h" | 12 #include "content/common/net_adapters.h" |
| 12 #include "content/public/common/resource_type.h" | 13 #include "content/public/common/resource_type.h" |
| 13 #include "net/http/http_status_code.h" | 14 #include "net/http/http_status_code.h" |
| 14 | 15 |
| 15 namespace content { | 16 namespace content { |
| 16 | 17 |
| 17 SubresourceLoadInfo::SubresourceLoadInfo() | 18 SubresourceLoadInfo::SubresourceLoadInfo() |
| 18 : routing_id(-1), request_id(-1), options(0) {} | 19 : routing_id(-1), request_id(-1), options(0) {} |
| 19 | 20 |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 69 AppCacheHistograms::AddNetworkJobStartDelaySample(base::TimeTicks::Now() - | 70 AppCacheHistograms::AddNetworkJobStartDelaySample(base::TimeTicks::Now() - |
| 70 start_time_tick_); | 71 start_time_tick_); |
| 71 | 72 |
| 72 if (IsResourceTypeFrame(request_.resource_type)) { | 73 if (IsResourceTypeFrame(request_.resource_type)) { |
| 73 DCHECK(!main_resource_loader_callback_.is_null()); | 74 DCHECK(!main_resource_loader_callback_.is_null()); |
| 74 // In network service land, if we are processing a navigation request, we | 75 // In network service land, if we are processing a navigation request, we |
| 75 // need to inform the loader callback that we are not going to handle this | 76 // need to inform the loader callback that we are not going to handle this |
| 76 // request. The loader callback is valid only for navigation requests. | 77 // request. The loader callback is valid only for navigation requests. |
| 77 std::move(main_resource_loader_callback_).Run(StartLoaderCallback()); | 78 std::move(main_resource_loader_callback_).Run(StartLoaderCallback()); |
| 78 } else { | 79 } else { |
| 80 mojom::URLLoaderClientPtr client_ptr; |
| 81 network_loader_client_binding_.Bind(mojo::MakeRequest(&client_ptr)); |
| 82 |
| 79 default_url_loader_factory_getter_->GetNetworkFactory() | 83 default_url_loader_factory_getter_->GetNetworkFactory() |
| 80 ->get() | 84 ->get() |
| 81 ->CreateLoaderAndStart( | 85 ->CreateLoaderAndStart( |
| 82 mojo::MakeRequest(&network_loader_request_), | 86 mojo::MakeRequest(&network_loader_), |
| 83 subresource_load_info_->routing_id, | 87 subresource_load_info_->routing_id, |
| 84 subresource_load_info_->request_id, subresource_load_info_->options, | 88 subresource_load_info_->request_id, subresource_load_info_->options, |
| 85 subresource_load_info_->request, std::move(client_info_), | 89 subresource_load_info_->request, std::move(client_ptr), |
| 86 subresource_load_info_->traffic_annotation); | 90 subresource_load_info_->traffic_annotation); |
| 87 } | 91 } |
| 88 } | 92 } |
| 89 | 93 |
| 90 void AppCacheURLLoaderJob::DeliverErrorResponse() { | 94 void AppCacheURLLoaderJob::DeliverErrorResponse() { |
| 91 delivery_type_ = ERROR_DELIVERY; | 95 delivery_type_ = ERROR_DELIVERY; |
| 92 | 96 |
| 93 // We expect the URLLoaderClient pointer to be valid at this point. | 97 // We expect the URLLoaderClient pointer to be valid at this point. |
| 94 DCHECK(client_info_); | 98 DCHECK(client_); |
| 95 | 99 |
| 96 // AppCacheURLRequestJob uses ERR_FAILED as the error code here. That seems | 100 // AppCacheURLRequestJob uses ERR_FAILED as the error code here. That seems |
| 97 // to map to HTTP_INTERNAL_SERVER_ERROR. | 101 // to map to HTTP_INTERNAL_SERVER_ERROR. |
| 98 std::string status("HTTP/1.1 "); | 102 std::string status("HTTP/1.1 "); |
| 99 status.append(base::IntToString(net::HTTP_INTERNAL_SERVER_ERROR)); | 103 status.append(base::IntToString(net::HTTP_INTERNAL_SERVER_ERROR)); |
| 100 status.append(" "); | 104 status.append(" "); |
| 101 status.append(net::GetHttpReasonPhrase(net::HTTP_INTERNAL_SERVER_ERROR)); | 105 status.append(net::GetHttpReasonPhrase(net::HTTP_INTERNAL_SERVER_ERROR)); |
| 102 status.append("\0\0", 2); | 106 status.append("\0\0", 2); |
| 103 | 107 |
| 104 ResourceResponseHead response; | 108 ResourceResponseHead response; |
| 105 response.headers = new net::HttpResponseHeaders(status); | 109 response.headers = new net::HttpResponseHeaders(status); |
| 106 client_info_->OnReceiveResponse(response, base::nullopt, nullptr); | 110 client_->OnReceiveResponse(response, base::nullopt, nullptr); |
| 107 | 111 |
| 108 NotifyCompleted(net::ERR_FAILED); | 112 NotifyCompleted(net::ERR_FAILED); |
| 109 | 113 |
| 110 AppCacheHistograms::AddErrorJobStartDelaySample(base::TimeTicks::Now() - | 114 AppCacheHistograms::AddErrorJobStartDelaySample(base::TimeTicks::Now() - |
| 111 start_time_tick_); | 115 start_time_tick_); |
| 112 } | 116 } |
| 113 | 117 |
| 114 const GURL& AppCacheURLLoaderJob::GetURL() const { | 118 const GURL& AppCacheURLLoaderJob::GetURL() const { |
| 115 return request_.url; | 119 return request_.url; |
| 116 } | 120 } |
| 117 | 121 |
| 118 AppCacheURLLoaderJob* AppCacheURLLoaderJob::AsURLLoaderJob() { | 122 AppCacheURLLoaderJob* AppCacheURLLoaderJob::AsURLLoaderJob() { |
| 119 return this; | 123 return this; |
| 120 } | 124 } |
| 121 | 125 |
| 122 void AppCacheURLLoaderJob::FollowRedirect() { | 126 void AppCacheURLLoaderJob::FollowRedirect() { |
| 123 if (network_loader_request_) | 127 if (network_loader_) |
| 124 network_loader_request_->FollowRedirect(); | 128 network_loader_->FollowRedirect(); |
| 125 } | 129 } |
| 126 | 130 |
| 127 void AppCacheURLLoaderJob::SetPriority(net::RequestPriority priority, | 131 void AppCacheURLLoaderJob::SetPriority(net::RequestPriority priority, |
| 128 int32_t intra_priority_value) { | 132 int32_t intra_priority_value) { |
| 129 if (network_loader_request_) | 133 if (network_loader_) |
| 130 network_loader_request_->SetPriority(priority, intra_priority_value); | 134 network_loader_->SetPriority(priority, intra_priority_value); |
| 135 } |
| 136 |
| 137 void AppCacheURLLoaderJob::OnReceiveResponse( |
| 138 const ResourceResponseHead& response_head, |
| 139 const base::Optional<net::SSLInfo>& ssl_info, |
| 140 mojom::DownloadedTempFilePtr downloaded_file) { |
| 141 appcache_request_->set_response(response_head); |
| 142 // The MaybeLoadFallbackForResponse() call below can pass a fallback |
| 143 // response to us. Reset the delivery_type_ to ensure that we can |
| 144 // receive it |
| 145 delivery_type_ = AWAITING_DELIVERY_ORDERS; |
| 146 if (!sub_resource_handler_->MaybeLoadFallbackForResponse(nullptr)) { |
| 147 client_->OnReceiveResponse(response_head, ssl_info, |
| 148 std::move(downloaded_file)); |
| 149 } else { |
| 150 // Disconnect from the network loader as we are delivering a fallback |
| 151 // response to the client. |
| 152 DisconnectFromNetworkLoader(); |
| 153 } |
| 154 } |
| 155 |
| 156 void AppCacheURLLoaderJob::OnReceiveRedirect( |
| 157 const net::RedirectInfo& redirect_info, |
| 158 const ResourceResponseHead& response_head) { |
| 159 appcache_request_->set_response(response_head); |
| 160 // The MaybeLoadFallbackForRedirect() call below can pass a fallback |
| 161 // response to us. Reset the delivery_type_ to ensure that we can |
| 162 // receive it |
| 163 delivery_type_ = AWAITING_DELIVERY_ORDERS; |
| 164 if (!sub_resource_handler_->MaybeLoadFallbackForRedirect( |
| 165 nullptr, redirect_info.new_url)) { |
| 166 client_->OnReceiveRedirect(redirect_info, response_head); |
| 167 } else { |
| 168 // Disconnect from the network loader as we are delivering a fallback |
| 169 // response to the client. |
| 170 DisconnectFromNetworkLoader(); |
| 171 } |
| 172 } |
| 173 |
| 174 void AppCacheURLLoaderJob::OnDataDownloaded(int64_t data_len, |
| 175 int64_t encoded_data_len) { |
| 176 client_->OnDataDownloaded(data_len, encoded_data_len); |
| 177 } |
| 178 |
| 179 void AppCacheURLLoaderJob::OnUploadProgress( |
| 180 int64_t current_position, |
| 181 int64_t total_size, |
| 182 OnUploadProgressCallback ack_callback) { |
| 183 client_->OnUploadProgress(current_position, total_size, |
| 184 std::move(ack_callback)); |
| 185 } |
| 186 |
| 187 void AppCacheURLLoaderJob::OnReceiveCachedMetadata( |
| 188 const std::vector<uint8_t>& data) { |
| 189 client_->OnReceiveCachedMetadata(data); |
| 190 } |
| 191 |
| 192 void AppCacheURLLoaderJob::OnTransferSizeUpdated(int32_t transfer_size_diff) { |
| 193 client_->OnTransferSizeUpdated(transfer_size_diff); |
| 194 } |
| 195 |
| 196 void AppCacheURLLoaderJob::OnStartLoadingResponseBody( |
| 197 mojo::ScopedDataPipeConsumerHandle body) { |
| 198 client_->OnStartLoadingResponseBody(std::move(body)); |
| 199 } |
| 200 |
| 201 void AppCacheURLLoaderJob::OnComplete( |
| 202 const ResourceRequestCompletionStatus& status) { |
| 203 delivery_type_ = AWAITING_DELIVERY_ORDERS; |
| 204 if (!sub_resource_handler_->MaybeLoadFallbackForResponse(nullptr)) { |
| 205 client_->OnComplete(status); |
| 206 } else { |
| 207 // Disconnect from the network loader as we are delivering a fallback |
| 208 // response to the client. |
| 209 DisconnectFromNetworkLoader(); |
| 210 } |
| 131 } | 211 } |
| 132 | 212 |
| 133 void AppCacheURLLoaderJob::SetSubresourceLoadInfo( | 213 void AppCacheURLLoaderJob::SetSubresourceLoadInfo( |
| 134 std::unique_ptr<SubresourceLoadInfo> subresource_load_info, | 214 std::unique_ptr<SubresourceLoadInfo> subresource_load_info, |
| 135 URLLoaderFactoryGetter* default_url_loader) { | 215 URLLoaderFactoryGetter* default_url_loader) { |
| 136 subresource_load_info_ = std::move(subresource_load_info); | 216 subresource_load_info_ = std::move(subresource_load_info); |
| 137 | 217 |
| 138 associated_binding_.reset(new mojo::AssociatedBinding<mojom::URLLoader>( | 218 associated_binding_.reset(new mojo::AssociatedBinding<mojom::URLLoader>( |
| 139 this, std::move(subresource_load_info_->url_loader_request))); | 219 this, std::move(subresource_load_info_->url_loader_request))); |
| 140 associated_binding_->set_connection_error_handler(base::Bind( | 220 associated_binding_->set_connection_error_handler(base::Bind( |
| 141 &AppCacheURLLoaderJob::OnConnectionError, StaticAsWeakPtr(this))); | 221 &AppCacheURLLoaderJob::OnConnectionError, StaticAsWeakPtr(this))); |
| 142 | 222 |
| 143 client_info_ = std::move(subresource_load_info_->client); | 223 client_ = std::move(subresource_load_info_->client); |
| 144 default_url_loader_factory_getter_ = default_url_loader; | 224 default_url_loader_factory_getter_ = default_url_loader; |
| 145 } | 225 } |
| 146 | 226 |
| 147 void AppCacheURLLoaderJob::Start(mojom::URLLoaderRequest request, | 227 void AppCacheURLLoaderJob::Start(mojom::URLLoaderRequest request, |
| 148 mojom::URLLoaderClientPtr client) { | 228 mojom::URLLoaderClientPtr client) { |
| 149 DCHECK(!binding_.is_bound()); | 229 DCHECK(!binding_.is_bound()); |
| 150 binding_.Bind(std::move(request)); | 230 binding_.Bind(std::move(request)); |
| 151 | 231 |
| 152 binding_.set_connection_error_handler(base::Bind( | 232 binding_.set_connection_error_handler(base::Bind( |
| 153 &AppCacheURLLoaderJob::OnConnectionError, StaticAsWeakPtr(this))); | 233 &AppCacheURLLoaderJob::OnConnectionError, StaticAsWeakPtr(this))); |
| 154 | 234 |
| 155 client_info_ = std::move(client); | 235 client_ = std::move(client); |
| 156 | 236 |
| 157 // Send the cached AppCacheResponse if any. | 237 // Send the cached AppCacheResponse if any. |
| 158 if (info_.get()) | 238 if (info_.get()) |
| 159 SendResponseInfo(); | 239 SendResponseInfo(); |
| 160 } | 240 } |
| 161 | 241 |
| 162 AppCacheURLLoaderJob::AppCacheURLLoaderJob(const ResourceRequest& request, | 242 AppCacheURLLoaderJob::AppCacheURLLoaderJob( |
| 163 AppCacheStorage* storage) | 243 const ResourceRequest& request, |
| 244 AppCacheURLLoaderRequest* appcache_request, |
| 245 AppCacheStorage* storage) |
| 164 : request_(request), | 246 : request_(request), |
| 165 storage_(storage->GetWeakPtr()), | 247 storage_(storage->GetWeakPtr()), |
| 166 start_time_tick_(base::TimeTicks::Now()), | 248 start_time_tick_(base::TimeTicks::Now()), |
| 167 cache_id_(kAppCacheNoCacheId), | 249 cache_id_(kAppCacheNoCacheId), |
| 168 is_fallback_(false), | 250 is_fallback_(false), |
| 169 binding_(this), | 251 binding_(this), |
| 170 writable_handle_watcher_(FROM_HERE, | 252 writable_handle_watcher_(FROM_HERE, |
| 171 mojo::SimpleWatcher::ArmingPolicy::MANUAL) {} | 253 mojo::SimpleWatcher::ArmingPolicy::MANUAL), |
| 254 network_loader_client_binding_(this), |
| 255 appcache_request_(appcache_request) {} |
| 172 | 256 |
| 173 void AppCacheURLLoaderJob::OnResponseInfoLoaded( | 257 void AppCacheURLLoaderJob::OnResponseInfoLoaded( |
| 174 AppCacheResponseInfo* response_info, | 258 AppCacheResponseInfo* response_info, |
| 175 int64_t response_id) { | 259 int64_t response_id) { |
| 176 DCHECK(IsDeliveringAppCacheResponse()); | 260 DCHECK(IsDeliveringAppCacheResponse()); |
| 177 | 261 |
| 178 if (!storage_.get()) { | 262 if (!storage_.get()) { |
| 179 DeliverErrorResponse(); | 263 DeliverErrorResponse(); |
| 180 return; | 264 return; |
| 181 } | 265 } |
| (...skipping 17 matching lines...) Expand all Loading... |
| 199 // TODO(ananta) | 283 // TODO(ananta) |
| 200 // Move the asynchronous reading and mojo pipe handling code to a helper | 284 // Move the asynchronous reading and mojo pipe handling code to a helper |
| 201 // class. That would also need a change to BlobURLLoader. | 285 // class. That would also need a change to BlobURLLoader. |
| 202 | 286 |
| 203 // Wait for the data pipe to be ready to accept data. | 287 // Wait for the data pipe to be ready to accept data. |
| 204 writable_handle_watcher_.Watch( | 288 writable_handle_watcher_.Watch( |
| 205 response_body_stream_.get(), MOJO_HANDLE_SIGNAL_WRITABLE, | 289 response_body_stream_.get(), MOJO_HANDLE_SIGNAL_WRITABLE, |
| 206 base::Bind(&AppCacheURLLoaderJob::OnResponseBodyStreamReady, | 290 base::Bind(&AppCacheURLLoaderJob::OnResponseBodyStreamReady, |
| 207 StaticAsWeakPtr(this))); | 291 StaticAsWeakPtr(this))); |
| 208 | 292 |
| 209 if (client_info_) | 293 if (client_) |
| 210 SendResponseInfo(); | 294 SendResponseInfo(); |
| 211 | 295 |
| 212 ReadMore(); | 296 ReadMore(); |
| 213 } else { | 297 } else { |
| 214 // Error case here. We fallback to the network. | 298 // Error case here. We fallback to the network. |
| 215 DeliverNetworkResponse(); | 299 DeliverNetworkResponse(); |
| 216 AppCacheHistograms::CountResponseRetrieval( | 300 AppCacheHistograms::CountResponseRetrieval( |
| 217 false, IsResourceTypeFrame(request_.resource_type), | 301 false, IsResourceTypeFrame(request_.resource_type), |
| 218 manifest_url_.GetOrigin()); | 302 manifest_url_.GetOrigin()); |
| 219 | 303 |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 253 ReadMore(); | 337 ReadMore(); |
| 254 } | 338 } |
| 255 | 339 |
| 256 void AppCacheURLLoaderJob::OnConnectionError() { | 340 void AppCacheURLLoaderJob::OnConnectionError() { |
| 257 if (storage_.get()) | 341 if (storage_.get()) |
| 258 storage_->CancelDelegateCallbacks(this); | 342 storage_->CancelDelegateCallbacks(this); |
| 259 base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this); | 343 base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this); |
| 260 } | 344 } |
| 261 | 345 |
| 262 void AppCacheURLLoaderJob::SendResponseInfo() { | 346 void AppCacheURLLoaderJob::SendResponseInfo() { |
| 263 DCHECK(client_info_); | 347 DCHECK(client_); |
| 264 // If this is null it means the response information was sent to the client. | 348 // If this is null it means the response information was sent to the client. |
| 265 if (!data_pipe_.consumer_handle.is_valid()) | 349 if (!data_pipe_.consumer_handle.is_valid()) |
| 266 return; | 350 return; |
| 267 | 351 |
| 268 const net::HttpResponseInfo* http_info = is_range_request() | 352 const net::HttpResponseInfo* http_info = is_range_request() |
| 269 ? range_response_info_.get() | 353 ? range_response_info_.get() |
| 270 : info_->http_response_info(); | 354 : info_->http_response_info(); |
| 271 | 355 |
| 272 ResourceResponseHead response_head; | 356 ResourceResponseHead response_head; |
| 273 response_head.headers = http_info->headers; | 357 response_head.headers = http_info->headers; |
| (...skipping 12 matching lines...) Expand all Loading... |
| 286 : info_->response_data_size(); | 370 : info_->response_data_size(); |
| 287 | 371 |
| 288 response_head.connection_info = http_info->connection_info; | 372 response_head.connection_info = http_info->connection_info; |
| 289 response_head.socket_address = http_info->socket_address; | 373 response_head.socket_address = http_info->socket_address; |
| 290 response_head.was_fetched_via_spdy = http_info->was_fetched_via_spdy; | 374 response_head.was_fetched_via_spdy = http_info->was_fetched_via_spdy; |
| 291 response_head.was_alpn_negotiated = http_info->was_alpn_negotiated; | 375 response_head.was_alpn_negotiated = http_info->was_alpn_negotiated; |
| 292 response_head.alpn_negotiated_protocol = http_info->alpn_negotiated_protocol; | 376 response_head.alpn_negotiated_protocol = http_info->alpn_negotiated_protocol; |
| 293 | 377 |
| 294 response_head.load_timing = load_timing_info_; | 378 response_head.load_timing = load_timing_info_; |
| 295 | 379 |
| 296 client_info_->OnReceiveResponse(response_head, http_info->ssl_info, | 380 appcache_request_->set_response(response_head); |
| 297 mojom::DownloadedTempFilePtr()); | |
| 298 | 381 |
| 299 client_info_->OnStartLoadingResponseBody( | 382 client_->OnReceiveResponse(response_head, http_info->ssl_info, |
| 300 std::move(data_pipe_.consumer_handle)); | 383 mojom::DownloadedTempFilePtr()); |
| 384 |
| 385 client_->OnStartLoadingResponseBody(std::move(data_pipe_.consumer_handle)); |
| 301 } | 386 } |
| 302 | 387 |
| 303 void AppCacheURLLoaderJob::ReadMore() { | 388 void AppCacheURLLoaderJob::ReadMore() { |
| 304 DCHECK(!pending_write_.get()); | 389 DCHECK(!pending_write_.get()); |
| 305 | 390 |
| 306 uint32_t num_bytes; | 391 uint32_t num_bytes; |
| 307 // TODO: we should use the abstractions in MojoAsyncResourceHandler. | 392 // TODO: we should use the abstractions in MojoAsyncResourceHandler. |
| 308 MojoResult result = NetToMojoPendingBuffer::BeginWrite( | 393 MojoResult result = NetToMojoPendingBuffer::BeginWrite( |
| 309 &response_body_stream_, &pending_write_, &num_bytes); | 394 &response_body_stream_, &pending_write_, &num_bytes); |
| 310 if (result == MOJO_RESULT_SHOULD_WAIT) { | 395 if (result == MOJO_RESULT_SHOULD_WAIT) { |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 356 // case of an error. | 441 // case of an error. |
| 357 if (!request_complete_data.error_code) { | 442 if (!request_complete_data.error_code) { |
| 358 request_complete_data.exists_in_cache = http_info->was_cached; | 443 request_complete_data.exists_in_cache = http_info->was_cached; |
| 359 request_complete_data.completion_time = base::TimeTicks::Now(); | 444 request_complete_data.completion_time = base::TimeTicks::Now(); |
| 360 request_complete_data.encoded_body_length = | 445 request_complete_data.encoded_body_length = |
| 361 is_range_request() ? range_response_info_->headers->GetContentLength() | 446 is_range_request() ? range_response_info_->headers->GetContentLength() |
| 362 : info_->response_data_size(); | 447 : info_->response_data_size(); |
| 363 request_complete_data.decoded_body_length = | 448 request_complete_data.decoded_body_length = |
| 364 request_complete_data.encoded_body_length; | 449 request_complete_data.encoded_body_length; |
| 365 } | 450 } |
| 366 client_info_->OnComplete(request_complete_data); | 451 client_->OnComplete(request_complete_data); |
| 452 } |
| 453 |
| 454 void AppCacheURLLoaderJob::DisconnectFromNetworkLoader() { |
| 455 // Close the pipe to the network loader as we are delivering a fallback |
| 456 // response to the client. |
| 457 network_loader_client_binding_.Close(); |
| 458 network_loader_ = nullptr; |
| 367 } | 459 } |
| 368 | 460 |
| 369 } // namespace content | 461 } // namespace content |
| OLD | NEW |