HTML代码过滤技术
参考资料:MSDN的《Pluggable Protocols Overview》
参考示例:MSDN提供的
http://support.microsoft.com/default.aspx?scid=kb;EN-US;q260840#appliesto
另一示例是用Delphi写的:http://www.guicode.com/scr/mimefilter.zip
要实现HTML代码过滤必需注册一个或多个MIME过滤器(Pluggable MIME Filter)。
MIME过滤器是一个COM对象,必需实现IInternetProtocolSink和IInternetProtocol接口。MIME过滤器可以注册成临时或者永久的,如果同时注册多个临时的MIME过滤器,那么后注册的对象先被调用!
要如何注册一个MIME过滤器?要注册一个永久的MIME过滤器,你必需在注册表
的HKEY_CLASSES_ROOT"PROTOCOLS"Filter键下添加一个子键,子键的名称是你要
注册的MIME类型,在添加的子键下必需有一个名为CLSID的字符串值,值的内容就是你
提供的COM对象的CLSID。该键的默认值可以是关于你的对象的简单描述。如果你用ATL
了开发,你可以在对象的RGS文件中添加如下内容:
HKCR
{
NoRemove PROTOCOLS
{
NoRemove Filter
{
ForceRemove 'text/html' = s 'XMLMimeFilter MIME Filter Sample'
{
val CLSID = s '{53B95211-7D77-11D2-9F80-00104B107C96}'
}
}
}
}
上面的代码来自文章开头提到的示例。’XMLMimeFilter MIME Filter Sample’和
{53B95211-7D77-11D2-9F80-00104B107C96}'都要换成你自已的!
如果要注册临时的MIME过滤器,就要通过IInternetSession接口(取消注册也
用该接口),下面提供的注册一个临时过滤器的代码:
CComPtr<IInternetSession> m_spSession ;
CComPtr<IClassFactory> m_spClassFactoryMime ;
hr = ::CoGetClassObject( CLSID_MimeFilter, CLSCTX_SERVER,
NULL, IID_IClassFactory,
(void**)&m_spClassFactoryMime );
if( hr == S_OK )
{
if( ::CoInternetGetSession( 0, &m_spSession, 0) ==S_OK )
{
m_spSession->RegisterMimeFilter(m_spClassFactoryMime,
CLSID_MimeFilter, L"text/html" );
}
}
这里的CLSID_MimeFilter就是你的对象的CLSID。
MIME的类型有许多种,要了解这些信息可以查看MSDN的附录
《MIME Type Detection in Internet Explorer 4.0》,但实际的类型要比这里列的多得多。
要了解你的电脑注册的MIME类型,可以查看注册表的[HKEY_CLASSES_ROOT"MIME"
Database"Content Type]键。也可以通过调用FindMimeFromData函数来得到文件的对应MIME
类型,下面的代码示例了得到JS文件的MIME类型:
LPWSTR pwzMimeOut ;
FindMimeFromData( NULL , L"time.js" , 0 , 0 , 0 , 0 , &pwzMimeOut , 0 );
得到的MIME类型是:application/x-javascript。
一般情况下,要过滤HMTL页面,可以注册text/html类型。你也可以根据实际情况
通过调用RegisterMimeFilter注册多个不同的MIME过滤器。
注册一个临时或永久的MIME过滤器后,接下来的工作就是实现MIME过滤器对象。
在实现之前,先看一下《Pluggable Protocols Overview》一文中关于MIME过滤器与WEB
处理器(transaction handler,即urlmon.dll)之间接口的调用的描述(注:urlmon.dll内部实现了IInternetProtocol和IInternetProtocolSink接口):
1、 WEB处理器调用MIME过滤器的IInternetProtocolRoot::Start方法(IInternetProtocol
从IInternetProtocolRoot派生);
2、 WEB处理器先后调用MIME过滤器的IInternetProtocolSink::ReportProgress 和
IInternetProtocolSink::ReportData方法;
3、 MIME过滤器调用WEB处理器的IInternetProtocol::Read方法;
4、 MIME过滤器调用WEB处理器的IInternetProtocolSink::ReportData方法;
5、 WEB处理器调用MIME过滤器的IInternetProtoco::Read方法;
因此,要实现MIME过滤器,有几个重要的方法:
1、IInternetProtocolRoot::Start方法:
HRESULT Start(
[in] LPCWSTR szUrl,
[in] IInternetProtocolSink *pOIProtSink,
[in] IInternetBindInfo *pOIBindInfo,
[in] DWORD grfPI,
[in] DWORD dwReserved
);
作为MIME过滤对象,szUrl传入的是MIME的类型(如果是name space handlers对象,
则该参数为一个即将下载或解析的URL)。若是你想得到URL,可以通过pOIBindInfo 接
口得到,下面是示例:
LPOLESTR pwzUrl ;
ULONG uElFetched ;
pIBindInfo->GetBindString( BINDSTRING_URL , &pwzUrl , 1 , &uElFetched ) ;
pOIProtSink是由urlmon.dll提供的IInternetProtocolSink接口,因为在后面的处理过程中,需要调用到该接口,所以要将它保存;
grfPI是一个枚举变量,必需包含PI_FILTER_MODE标志,表示该对象运行在filter模式中。
dwReserved是一个指向PROTOCOLFILTERDATA结构的指针,该结构的pProtocol成员是由urlmon.dll提供的IInternetProtocol接口,因为在后面的处理过程中需要调用到该接口,所以要将它保存。实际上该接口也可以通过pOIProtSink参数调用QueryInterface得到,同样PROTOCOLFILTERDATA结构的pProtocolSink与pOIProtSink都是指向同一个接口。
在Start方法中,我们必需做的实际上只是保存urlmon.dll提供的IInternetProtocolSink
和IInternetProtocol接口。
2、IInternetProtocolSink::ReportProgress方法:
HRESULT ReportProgress(
[in] ULONG ulStatusCode,
[in] LPCWSTR szStatusText )
作为MIME过滤器,ulStatusCode一般都是BINDSTATUS_CACHEFILENAMEAVAILABLE , 当
ulStatusCode为BINDSTATUS_CACHEFILENAMEAVAILABLE时,szStatusText为临时缓存文件
的路径名称,但有一些网页并不写到缓存里,所以szStatusText可能为空字符串。
3、IInternetProtocolSink::ReportData方法:
HRESULT ReportData(
[in] DWORD grfBSCF,
[in] ULONG ulProgress,
[in] ULONG ulProgressMax
);
IE下载文件过程中或下载完毕时会调用MIME过滤器的ReportData方法,ulProgressMax
为文件总是数据量,ulProgress为下载进度,理论上当文件全部下载完后,ulProgress应
等于ulProgressMax(实际上,当网页文件不是很大时,即使ulProgress不等于ulProgressMax时,文件也可能全部下载下来),还有一个反应文件下载情况的参数是grfBSCF。有时,ReportData方法会被Web处理器调用多次。
ReportData是过滤网页内容或修改网页内容比较合适的地方。在此地,可以将网页内容通过调用Read保存到自已的缓存或流中并做适当的处理(注意检查字符的编码)。
最后,别忘了调用Web处理器的IInternetProtocolSink::ReportData方法,向它汇报数据下载的情况。Web处理器得到此通知后,就会调用MIME过滤器的IInternetProtocol::Read,此时,你就可以将修改后的数据交给WEB处理器。
下面的代码示例了如何在ReportData中调用Web处理器的Read预先保存数据:
// m_spIncomingProt是在Start中保存的Web处理器的IInternetProtocol接口
// m_spStm 是IStream 指针,用来缓存数据
BYTE buffer[ SIZE_BUFFER ] ;
DWORD cbRead ;
do
{
cbRead = 0 ;
hr = m_spIncomingProt->Read( buffer , SIZE_BUFFER , &cbRead ) ;
if( cbRead > 0 ) **A 处
{
if( m_spStm->Write( buffer , cbRead , NULL ) == S_OK )
{
m_cbTotal += cbRead ;
}
}
}while ( hr == S_OK );
Read成功取得数据一般只返回S_OK或S_FALSE ,返回S_OK表示还有数据,而S_FALSE
表示数据已读取完毕,因此循环的条件设为 hr==S_OK。那A处的条件判断为什么不是
if( hr == S_OK || hr == S_FALSE ) 呢, 因为我发现某些情况下,Read可能返回其
它值,但仍然有成功读取一部分数据出来,数据的大小就是cbRead指定的值。如果将
那部分数据遗落,网页将无法正常解析!那这样会不会导致因读取失败而将一些无用的
数据保存到缓存中?至少目前还没碰到过。
4、IInternetProtocol::Read方法
该方法由WEB处理器调用来取得浏览器要解析的数据。在上一方法ReportData中
我们已经将所有数据缓存到流中,因此,这里只需将流中的数据返回给WEB处理器。
下面的代码示例了Read中的简单处理:
if( m_spStm->Read( pv , cb , pcbRead ) == S_OK )
{
if( *pcbRead == cb )
{
return S_OK ;
}
else
return S_FALSE ;
}
千万注意,在数据已读取完毕时要返回S_FALSE , 不然可能导致Read被无穷循环调用。
处理完这几个方法后,基本是大功造成,其它一些方法处理十分简单,可以参考上面
提到的例子。