1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome_frame/urlmon_bind_status_callback.h" 6 7 #include <mshtml.h> 8 #include <shlguid.h> 9 10 #include "base/logging.h" 11 #include "base/strings/string_util.h" 12 #include "base/strings/stringprintf.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "base/threading/platform_thread.h" 15 #include "chrome_frame/bind_context_info.h" 16 #include "chrome_frame/chrome_tab.h" 17 #include "chrome_frame/exception_barrier.h" 18 #include "chrome_frame/urlmon_moniker.h" 19 20 21 // A helper to given feed data to the specified |bscb| using 22 // CacheStream instance. 23 HRESULT CacheStream::BSCBFeedData(IBindStatusCallback* bscb, const char* data, 24 size_t size, CLIPFORMAT clip_format, 25 size_t flags, bool eof) { 26 if (!bscb) { 27 NOTREACHED() << "invalid IBindStatusCallback"; 28 return E_INVALIDARG; 29 } 30 31 // We can't use a CComObjectStackEx here since mshtml will hold 32 // onto the stream pointer. 33 CComObject<CacheStream>* cache_stream = NULL; 34 HRESULT hr = CComObject<CacheStream>::CreateInstance(&cache_stream); 35 if (FAILED(hr)) { 36 NOTREACHED(); 37 return hr; 38 } 39 40 scoped_refptr<CacheStream> cache_ref = cache_stream; 41 hr = cache_stream->Initialize(data, size, eof); 42 if (FAILED(hr)) 43 return hr; 44 45 FORMATETC format_etc = { clip_format, NULL, DVASPECT_CONTENT, -1, 46 TYMED_ISTREAM }; 47 STGMEDIUM medium = {0}; 48 medium.tymed = TYMED_ISTREAM; 49 medium.pstm = cache_stream; 50 51 hr = bscb->OnDataAvailable(flags, size, &format_etc, &medium); 52 return hr; 53 } 54 55 HRESULT CacheStream::Initialize(const char* cache, size_t size, bool eof) { 56 position_ = 0; 57 eof_ = eof; 58 59 HRESULT hr = S_OK; 60 cache_.reset(new char[size]); 61 if (cache_.get()) { 62 memcpy(cache_.get(), cache, size); 63 size_ = size; 64 } else { 65 DLOG(ERROR) << "failed to allocate cache stream."; 66 hr = E_OUTOFMEMORY; 67 } 68 69 return hr; 70 } 71 72 // Read is the only call that we expect. Return E_PENDING if there 73 // is no more data to serve. Otherwise this will result in a 74 // read with 0 bytes indicating that no more data is available. 75 STDMETHODIMP CacheStream::Read(void* pv, ULONG cb, ULONG* read) { 76 if (!pv || !read) 77 return E_INVALIDARG; 78 79 if (!cache_.get()) { 80 *read = 0; 81 return S_FALSE; 82 } 83 84 // Default to E_PENDING to signal that this is a partial data. 85 HRESULT hr = eof_ ? S_FALSE : E_PENDING; 86 if (position_ < size_) { 87 *read = std::min(size_ - position_, size_t(cb)); 88 memcpy(pv, cache_ .get() + position_, *read); 89 position_ += *read; 90 hr = S_OK; 91 } 92 93 return hr; 94 } 95 96 97 ///////////////////////////////////////////////////////////////////// 98 99 HRESULT SniffData::InitializeCache(const std::wstring& url) { 100 url_ = url; 101 renderer_type_ = UNDETERMINED; 102 103 const int kInitialSize = 4 * 1024; // 4K 104 HGLOBAL mem = GlobalAlloc(0, kInitialSize); 105 DCHECK(mem) << "GlobalAlloc failed: " << GetLastError(); 106 107 HRESULT hr = CreateStreamOnHGlobal(mem, TRUE, cache_.Receive()); 108 if (SUCCEEDED(hr)) { 109 ULARGE_INTEGER size = {0}; 110 cache_->SetSize(size); 111 } else { 112 DLOG(ERROR) << "CreateStreamOnHGlobal failed: " << hr; 113 } 114 115 return hr; 116 } 117 118 HRESULT SniffData::ReadIntoCache(IStream* stream, bool force_determination) { 119 if (!stream) { 120 NOTREACHED(); 121 return E_INVALIDARG; 122 } 123 124 HRESULT hr = S_OK; 125 while (SUCCEEDED(hr)) { 126 const size_t kChunkSize = 4 * 1024; 127 char buffer[kChunkSize]; 128 DWORD read = 0; 129 hr = stream->Read(buffer, sizeof(buffer), &read); 130 if (read) { 131 DWORD written = 0; 132 cache_->Write(buffer, read, &written); 133 size_ += written; 134 } 135 136 if ((S_FALSE == hr) || !read) 137 break; 138 } 139 140 bool last_chance = force_determination || (size() >= kMaxSniffSize); 141 eof_ = force_determination; 142 DetermineRendererType(last_chance); 143 return hr; 144 } 145 146 HRESULT SniffData::DrainCache(IBindStatusCallback* bscb, DWORD bscf, 147 CLIPFORMAT clip_format) { 148 if (!is_cache_valid()) { 149 return S_OK; 150 } 151 152 // Ideally we could just use the cache_ IStream implementation but 153 // can't use it here since we have to return E_PENDING for the 154 // last call 155 HGLOBAL memory = NULL; 156 HRESULT hr = GetHGlobalFromStream(cache_, &memory); 157 if (SUCCEEDED(hr) && memory) { 158 char* buffer = reinterpret_cast<char*>(GlobalLock(memory)); 159 hr = CacheStream::BSCBFeedData(bscb, buffer, size_, clip_format, bscf, 160 eof_); 161 GlobalUnlock(memory); 162 } 163 164 size_ = 0; 165 cache_.Release(); 166 return hr; 167 } 168 169 // Scan the buffer or OptIn URL list and decide if the renderer is 170 // to be switched. Last chance means there's no more data. 171 void SniffData::DetermineRendererType(bool last_chance) { 172 if (is_undetermined()) { 173 if (last_chance) 174 renderer_type_ = OTHER; 175 if (IsChrome(RendererTypeForUrl(url_))) { 176 renderer_type_ = CHROME; 177 } else { 178 if (is_cache_valid() && cache_) { 179 HGLOBAL memory = NULL; 180 GetHGlobalFromStream(cache_, &memory); 181 const char* buffer = reinterpret_cast<const char*>(GlobalLock(memory)); 182 183 std::wstring html_contents; 184 // TODO(joshia): detect and handle different content encodings 185 if (buffer && size_) { 186 UTF8ToWide(buffer, std::min(size_, kMaxSniffSize), &html_contents); 187 GlobalUnlock(memory); 188 } 189 190 // Note that document_contents_ may have NULL characters in it. While 191 // browsers may handle this properly, we don't and will stop scanning 192 // for the XUACompat content value if we encounter one. 193 std::wstring xua_compat_content; 194 UtilGetXUACompatContentValue(html_contents, &xua_compat_content); 195 if (StrStrI(xua_compat_content.c_str(), kChromeContentPrefix)) { 196 renderer_type_ = CHROME; 197 } 198 } 199 } 200 DVLOG(1) << __FUNCTION__ << "Url: " << url_ << base::StringPrintf( 201 "Renderer type: %s", renderer_type_ == CHROME ? "CHROME" : "OTHER"); 202 } 203 } 204 205 ///////////////////////////////////////////////////////////////////// 206 207 BSCBStorageBind::BSCBStorageBind() : clip_format_(CF_NULL) { 208 } 209 210 BSCBStorageBind::~BSCBStorageBind() { 211 std::for_each(saved_progress_.begin(), saved_progress_.end(), 212 utils::DeleteObject()); 213 } 214 215 HRESULT BSCBStorageBind::Initialize(IMoniker* moniker, IBindCtx* bind_ctx) { 216 DVLOG(1) << __FUNCTION__ << me() 217 << base::StringPrintf(" tid=%i", base::PlatformThread::CurrentId()); 218 219 std::wstring url = GetActualUrlFromMoniker(moniker, bind_ctx, 220 std::wstring()); 221 HRESULT hr = data_sniffer_.InitializeCache(url); 222 if (FAILED(hr)) 223 return hr; 224 225 hr = AttachToBind(bind_ctx); 226 if (FAILED(hr)) { 227 NOTREACHED() << __FUNCTION__ << me() << "AttachToBind error: " << hr; 228 return hr; 229 } 230 231 if (!delegate()) { 232 NOTREACHED() << __FUNCTION__ << me() << "No existing callback: " << hr; 233 return E_FAIL; 234 } 235 236 return hr; 237 } 238 239 STDMETHODIMP BSCBStorageBind::OnProgress(ULONG progress, ULONG progress_max, 240 ULONG status_code, LPCWSTR status_text) { 241 DVLOG(1) << __FUNCTION__ << me() 242 << base::StringPrintf(" status=%i tid=%i %ls", status_code, 243 base::PlatformThread::CurrentId(), 244 status_text); 245 // Report all crashes in the exception handler if we wrap the callback. 246 // Note that this avoids having the VEH report a crash if an SEH earlier in 247 // the chain handles the exception. 248 ExceptionBarrier barrier; 249 250 HRESULT hr = S_OK; 251 252 // TODO(ananta) 253 // ChromeFrame will not be informed of any redirects which occur while we 254 // switch into Chrome. This will only break the moniker patch which is 255 // legacy and needs to be deleted. 256 257 if (ShouldCacheProgress(status_code)) { 258 saved_progress_.push_back(new Progress(progress, progress_max, status_code, 259 status_text)); 260 } else { 261 hr = CallbackImpl::OnProgress(progress, progress_max, status_code, 262 status_text); 263 } 264 265 return hr; 266 } 267 268 // Refer to urlmon_moniker.h for explanation of how things work. 269 STDMETHODIMP BSCBStorageBind::OnDataAvailable(DWORD flags, DWORD size, 270 FORMATETC* format_etc, 271 STGMEDIUM* stgmed) { 272 DVLOG(1) << __FUNCTION__ 273 << base::StringPrintf(" tid=%i", base::PlatformThread::CurrentId()); 274 // Report all crashes in the exception handler if we wrap the callback. 275 // Note that this avoids having the VEH report a crash if an SEH earlier in 276 // the chain handles the exception. 277 ExceptionBarrier barrier; 278 // Do not touch anything other than text/html. 279 bool is_interesting = (format_etc && stgmed && stgmed->pstm && 280 stgmed->tymed == TYMED_ISTREAM && 281 IsTextHtmlClipFormat(format_etc->cfFormat)); 282 283 if (!is_interesting) { 284 // Play back report progress so far. 285 MayPlayBack(flags); 286 return CallbackImpl::OnDataAvailable(flags, size, format_etc, stgmed); 287 } 288 289 HRESULT hr = S_OK; 290 if (!clip_format_) 291 clip_format_ = format_etc->cfFormat; 292 293 if (data_sniffer_.is_undetermined()) { 294 bool force_determination = !!(flags & 295 (BSCF_LASTDATANOTIFICATION | BSCF_DATAFULLYAVAILABLE)); 296 hr = data_sniffer_.ReadIntoCache(stgmed->pstm, force_determination); 297 // If we don't have sufficient data to determine renderer type 298 // wait for the next data notification. 299 if (data_sniffer_.is_undetermined()) 300 return S_OK; 301 } 302 303 DCHECK(!data_sniffer_.is_undetermined()); 304 305 if (data_sniffer_.is_cache_valid()) { 306 hr = MayPlayBack(flags); 307 DCHECK(!data_sniffer_.is_cache_valid()); 308 } else { 309 hr = CallbackImpl::OnDataAvailable(flags, size, format_etc, stgmed); 310 } 311 return hr; 312 } 313 314 STDMETHODIMP BSCBStorageBind::OnStopBinding(HRESULT hresult, LPCWSTR error) { 315 DVLOG(1) << __FUNCTION__ 316 << base::StringPrintf(" tid=%i", base::PlatformThread::CurrentId()); 317 // Report all crashes in the exception handler if we wrap the callback. 318 // Note that this avoids having the VEH report a crash if an SEH earlier in 319 // the chain handles the exception. 320 ExceptionBarrier barrier; 321 322 HRESULT hr = MayPlayBack(BSCF_LASTDATANOTIFICATION); 323 if (FAILED(hr)) 324 return hr; 325 hr = CallbackImpl::OnStopBinding(hresult, error); 326 ReleaseBind(); 327 return hr; 328 } 329 330 // Play back the cached data to the delegate. Normally this would happen 331 // when we have read enough data to determine the renderer. In this case 332 // we first play back the data from the cache and then go into a 'pass 333 // through' mode. In some cases we may end up getting OnStopBinding 334 // before we get a chance to determine. Also it's possible that the 335 // BindToStorage call will return before OnStopBinding is sent. Hence 336 // This is called from 3 places and it's important to maintain the 337 // exact sequence of calls. 338 // Once the data is played back, calling this again is a no op. 339 HRESULT BSCBStorageBind::MayPlayBack(DWORD flags) { 340 // Force renderer type determination if not already done since 341 // we want to play back data now. 342 data_sniffer_.DetermineRendererType(true); 343 DCHECK(!data_sniffer_.is_undetermined()); 344 345 HRESULT hr = S_OK; 346 if (data_sniffer_.is_chrome()) { 347 // Remember clip format. If we are switching to chrome, then in order 348 // to make mshtml return INET_E_TERMINATED_BIND and reissue navigation 349 // with the same bind context, we have to return a mime type that is 350 // special cased by mshtml. 351 static const CLIPFORMAT kMagicClipFormat = 352 RegisterClipboardFormat(CFSTR_MIME_MPEG); 353 clip_format_ = kMagicClipFormat; 354 } else { 355 if (!saved_progress_.empty()) { 356 for (ProgressVector::iterator i = saved_progress_.begin(); 357 i != saved_progress_.end(); i++) { 358 Progress* p = (*i); 359 // We don't really expect a race condition here but just for sake 360 // of completeness we check. 361 if (p) { 362 (*i) = NULL; 363 CallbackImpl::OnProgress(p->progress(), p->progress_max(), 364 p->status_code(), p->status_text()); 365 delete p; 366 } 367 } 368 saved_progress_.clear(); 369 } 370 } 371 372 if (data_sniffer_.is_cache_valid()) { 373 if (data_sniffer_.is_chrome()) { 374 base::win::ScopedComPtr<BindContextInfo> info; 375 BindContextInfo::FromBindContext(bind_ctx_, info.Receive()); 376 DCHECK(info); 377 if (info) { 378 info->SetToSwitch(data_sniffer_.cache_); 379 } 380 } 381 382 hr = data_sniffer_.DrainCache(delegate(), 383 flags | BSCF_FIRSTDATANOTIFICATION, clip_format_); 384 DLOG_IF(WARNING, INET_E_TERMINATED_BIND != hr) << __FUNCTION__ << 385 " mshtml OnDataAvailable returned: " << std::hex << hr; 386 } 387 388 return hr; 389 } 390 391 // We cache and suppress sending progress notifications till 392 // we get the first OnDataAvailable. This is to prevent 393 // mshtml from making up its mind about the mime type. 394 // However, this is the invasive part of the patch and 395 // could trip other software that's due to mistimed progress 396 // notifications. It is probably not a good idea to hide redirects 397 // and some cookie notifications. 398 // 399 // We only need to suppress data notifications like 400 // BINDSTATUS_MIMETYPEAVAILABLE, 401 // BINDSTATUS_CACHEFILENAMEAVAILABLE etc. 402 // 403 // This is an atempt to reduce the exposure by starting to 404 // cache only when we receive one of the interesting progress 405 // notification. 406 bool BSCBStorageBind::ShouldCacheProgress(unsigned long status_code) const { 407 // We need to cache progress notifications only if we haven't yet figured 408 // out which way the request is going. 409 if (data_sniffer_.is_undetermined()) { 410 // If we are already caching then continue. 411 if (!saved_progress_.empty()) 412 return true; 413 // Start caching only if we see one of the interesting progress 414 // notifications. 415 switch (status_code) { 416 case BINDSTATUS_BEGINDOWNLOADDATA: 417 case BINDSTATUS_DOWNLOADINGDATA: 418 case BINDSTATUS_USINGCACHEDCOPY: 419 case BINDSTATUS_MIMETYPEAVAILABLE: 420 case BINDSTATUS_CACHEFILENAMEAVAILABLE: 421 case BINDSTATUS_SERVER_MIMETYPEAVAILABLE: 422 return true; 423 default: 424 break; 425 } 426 } 427 428 return false; 429 } 430