You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2403 lines
69 KiB
2403 lines
69 KiB
#include "private.h"
|
|
#include "subsmgrp.h"
|
|
#include "downld.h"
|
|
#include "chanmgr.h"
|
|
#include "chanmgrp.h"
|
|
#include "helper.h"
|
|
#include "shguidp.h" // IID_IChannelMgrPriv
|
|
|
|
#undef TF_THISMODULE
|
|
#define TF_THISMODULE TF_CDFAGENT
|
|
|
|
|
|
const int MINUTES_PER_DAY = 24 * 60;
|
|
|
|
|
|
//==============================================================================
|
|
// Convert an XML OM to a TaskTrigger by looking for and converting <schedule>
|
|
//==============================================================================
|
|
// returns S_OK if succeeded. (S_FALSE if succeeded but TASK_TRIGGER was truncated).
|
|
// returns E_FAIL if no task trigger retrieved (returned *ptt is invalid TASK_TRIGGER)
|
|
// You Must fill in ptt->cbTriggerSize!!!
|
|
// User can pass in Schedule element itself, or any parent element, in pRootEle
|
|
HRESULT XMLScheduleElementToTaskTrigger(IXMLElement *pRootEle, TASK_TRIGGER *ptt)
|
|
{
|
|
HRESULT hr = E_FAIL;
|
|
|
|
if (!pRootEle || !ptt)
|
|
return E_INVALIDARG;
|
|
|
|
ASSERT(ptt->cbTriggerSize == sizeof(TASK_TRIGGER));
|
|
|
|
CExtractSchedule *pSched = new CExtractSchedule(pRootEle, NULL);
|
|
|
|
if (pSched)
|
|
{
|
|
if (SUCCEEDED(pSched->Run()))
|
|
{
|
|
hr = pSched->GetTaskTrigger(ptt);
|
|
}
|
|
|
|
delete pSched;
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
// CExtractSchedule doesn't get used during channel update
|
|
// It's just used to traverse the OM and find the first Schedule tag, to
|
|
// parse out the schedule info
|
|
CExtractSchedule::CExtractSchedule(IXMLElement *pEle, CExtractSchedule *pExtractRoot) :
|
|
CProcessElement(NULL, NULL, pEle)
|
|
{
|
|
m_pExtractRoot = pExtractRoot;
|
|
if (!pExtractRoot)
|
|
m_pExtractRoot = this;
|
|
}
|
|
|
|
HRESULT CExtractSchedule::Run()
|
|
{
|
|
// Allow user to pass in Schedule element itself, or Root element
|
|
BSTR bstrItem=NULL;
|
|
HRESULT hr;
|
|
|
|
m_pElement->get_tagName(&bstrItem);
|
|
|
|
if (bstrItem && *bstrItem && !StrCmpIW(bstrItem, L"Schedule"))
|
|
{
|
|
hr = ProcessItemInEnum(bstrItem, m_pElement);
|
|
}
|
|
else
|
|
{
|
|
hr = CProcessElement::Run();
|
|
}
|
|
|
|
SysFreeString(bstrItem);
|
|
return hr;
|
|
}
|
|
|
|
HRESULT CExtractSchedule::ProcessItemInEnum(LPCWSTR pwszTagName, IXMLElement *pItem)
|
|
{
|
|
if (!StrCmpIW(pwszTagName, L"Schedule"))
|
|
{
|
|
CProcessSchedule *pPS = new CProcessSchedule(this, NULL, pItem);
|
|
if (pPS)
|
|
{
|
|
pPS->Run();
|
|
|
|
if (pPS->m_tt.cbTriggerSize)
|
|
{
|
|
ASSERT(pPS->m_tt.cbTriggerSize == sizeof(m_tt));
|
|
m_pExtractRoot->m_tt = pPS->m_tt;
|
|
}
|
|
|
|
delete pPS;
|
|
}
|
|
return E_ABORT; // abort our enumerations
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"Channel"))
|
|
{
|
|
return DoChild(new CExtractSchedule(pItem, m_pExtractRoot));
|
|
}
|
|
|
|
return S_OK; // ignore other tags
|
|
}
|
|
|
|
HRESULT CExtractSchedule::GetTaskTrigger(TASK_TRIGGER *ptt)
|
|
{
|
|
if ((0 == m_tt.cbTriggerSize) || // No task trigger
|
|
(0 == m_tt.wBeginYear)) // Invalid task trigger
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
if (m_tt.cbTriggerSize <= ptt->cbTriggerSize)
|
|
{
|
|
*ptt = m_tt;
|
|
return S_OK;
|
|
}
|
|
|
|
WORD cbTriggerSize = ptt->cbTriggerSize;
|
|
|
|
CopyMemory(ptt, &m_tt, cbTriggerSize);
|
|
ptt->cbTriggerSize = cbTriggerSize;
|
|
|
|
return S_FALSE;
|
|
}
|
|
|
|
//==============================================================================
|
|
// XML OM Helper functions
|
|
//==============================================================================
|
|
HRESULT GetXMLAttribute(IXMLElement *pItem, LPCWSTR pwszAttribute, VARIANT *pvRet)
|
|
{
|
|
BSTR bstrName=NULL;
|
|
HRESULT hr=E_FAIL;
|
|
|
|
pvRet->vt = VT_EMPTY;
|
|
bstrName = SysAllocString(pwszAttribute);
|
|
if (bstrName && SUCCEEDED(pItem->getAttribute(bstrName, pvRet)))
|
|
{
|
|
hr = S_OK;
|
|
}
|
|
SysFreeString(bstrName);
|
|
return hr;
|
|
}
|
|
|
|
HRESULT GetXMLStringAttribute(IXMLElement *pItem, LPCWSTR pwszAttribute, BSTR *pbstrRet)
|
|
{
|
|
VARIANT var;
|
|
BSTR bstrName=NULL;
|
|
HRESULT hr=E_FAIL;
|
|
|
|
*pbstrRet = NULL;
|
|
|
|
var.vt = VT_EMPTY;
|
|
bstrName = SysAllocString(pwszAttribute);
|
|
if (bstrName && SUCCEEDED(pItem->getAttribute(bstrName, &var)))
|
|
{
|
|
if (var.vt == VT_BSTR && var.bstrVal != NULL)
|
|
{
|
|
*pbstrRet = var.bstrVal;
|
|
|
|
hr = S_OK;
|
|
}
|
|
}
|
|
SysFreeString(bstrName);
|
|
if (FAILED(hr) && var.vt != VT_EMPTY)
|
|
VariantClear(&var);
|
|
|
|
return hr;
|
|
}
|
|
|
|
DWORD GetXMLDwordAttribute(IXMLElement *pItem, LPCWSTR pwszAttribute, DWORD dwDefault)
|
|
{
|
|
VARIANT var;
|
|
|
|
if (SUCCEEDED(GetXMLAttribute(pItem, pwszAttribute, &var)))
|
|
{
|
|
if (var.vt == VT_I4)
|
|
return var.lVal;
|
|
|
|
if (var.vt == VT_I2)
|
|
return var.iVal;
|
|
|
|
if (var.vt == VT_BSTR)
|
|
{
|
|
LPCWSTR pwsz = var.bstrVal;
|
|
DWORD dwRet;
|
|
|
|
if (!StrToIntExW(pwsz, 0, (int *)&dwRet))
|
|
dwRet = dwDefault;
|
|
|
|
SysFreeString(var.bstrVal);
|
|
return dwRet;
|
|
}
|
|
|
|
VariantClear(&var);
|
|
}
|
|
|
|
return dwDefault;
|
|
}
|
|
|
|
// If failure return code, *pfRet wasn't changed
|
|
HRESULT GetXMLBoolAttribute(IXMLElement *pItem, LPCWSTR pwszAttribute, BOOL *pfRet)
|
|
{
|
|
VARIANT var;
|
|
HRESULT hr=E_FAIL;
|
|
|
|
if (SUCCEEDED(GetXMLAttribute(pItem, pwszAttribute, &var)))
|
|
{
|
|
if (var.vt == VT_BOOL)
|
|
{
|
|
*pfRet = (var.boolVal == VARIANT_TRUE);
|
|
hr = S_OK;
|
|
}
|
|
else if (var.vt == VT_BSTR)
|
|
{
|
|
if (!StrCmpIW(var.bstrVal, L"YES") ||
|
|
!StrCmpIW(var.bstrVal, L"\"YES\""))
|
|
{
|
|
*pfRet = TRUE;
|
|
hr = S_OK;
|
|
}
|
|
else if (!StrCmpIW(var.bstrVal, L"NO") ||
|
|
!StrCmpIW(var.bstrVal, L"\"NO\""))
|
|
{
|
|
*pfRet = FALSE;
|
|
hr = S_OK;
|
|
}
|
|
}
|
|
else
|
|
hr = E_FAIL;
|
|
|
|
VariantClear(&var);
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
HRESULT GetXMLTimeAttributes(IXMLElement *pItem, CDF_TIME *pTime)
|
|
{
|
|
pTime->wReserved = 0;
|
|
|
|
pTime->wDay = (WORD) GetXMLDwordAttribute(pItem, L"DAY", 0);
|
|
pTime->wHour = (WORD) GetXMLDwordAttribute(pItem, L"HOUR", 0);
|
|
pTime->wMin = (WORD) GetXMLDwordAttribute(pItem, L"MIN", 0);
|
|
|
|
pTime->dwConvertedMinutes = (24 * 60 * pTime->wDay) +
|
|
( 60 * pTime->wHour) +
|
|
( pTime->wMin);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
inline BOOL IsNumber(WCHAR x) { return (x >= L'0' && x <= L'9'); }
|
|
|
|
HRESULT GetXMLTimeZoneAttribute(IXMLElement *pItem, LPCWSTR pwszAttribute, int *piRet)
|
|
{
|
|
BSTR bstrVal;
|
|
HRESULT hrRet = E_FAIL;
|
|
|
|
ASSERT(pItem && piRet);
|
|
|
|
if (SUCCEEDED(GetXMLStringAttribute(pItem, pwszAttribute, &bstrVal)))
|
|
{
|
|
if(bstrVal && bstrVal[0] &&
|
|
IsNumber(bstrVal[1]) && IsNumber(bstrVal[2]) &&
|
|
IsNumber(bstrVal[3]) && IsNumber(bstrVal[4]))
|
|
{
|
|
*piRet = 1000*(bstrVal[1] - L'0') +
|
|
100*(bstrVal[2] - L'0') +
|
|
10*(bstrVal[3] - L'0') +
|
|
bstrVal[4] - L'0';
|
|
|
|
hrRet = S_OK;
|
|
}
|
|
if(bstrVal[0] == L'-')
|
|
*piRet *= -1;
|
|
}
|
|
|
|
SysFreeString(bstrVal);
|
|
|
|
return hrRet;
|
|
}
|
|
|
|
//==============================================================================
|
|
// TEMP fn to convert ISO 1234:5678 to SYSTEMTIME
|
|
// ISODateToSystemTime returns false if there is a parse error
|
|
// true if there isn't
|
|
//==============================================================================
|
|
BOOL ValidateSystemTime(SYSTEMTIME *time)
|
|
{
|
|
// IE6 27665. Some incompetent XML file writers provide slightly invalid dates, like 9-31-01.
|
|
// Everybody knows that September has 30 days. Anyway, we'll do some minimal fix-up here.
|
|
switch (time->wMonth)
|
|
{
|
|
case 2: // February
|
|
// Rule is, every four years is a leap year, except for centuries.
|
|
if ((time->wYear % 4) || ((time->wYear % 100)==0))
|
|
{
|
|
if (time->wDay > 28)
|
|
{
|
|
time->wDay = 28;
|
|
}
|
|
}
|
|
else if (time->wDay > 29)
|
|
{
|
|
time->wDay = 29;
|
|
}
|
|
break;
|
|
|
|
case 1: // January
|
|
case 3: // March
|
|
case 5: // May
|
|
case 7: // July
|
|
case 8: // August
|
|
case 10: // October
|
|
case 12: // December
|
|
if (time->wDay>31)
|
|
{
|
|
time->wDay = 31;
|
|
}
|
|
break;
|
|
|
|
default: // the other 4 months. These have 30 days, apparently.
|
|
if (time->wDay>30)
|
|
{
|
|
time->wDay = 30;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
// yyyy-mm-dd[Thh:mm[+zzzz]]
|
|
BOOL ISODateToSystemTime(LPCWSTR string, SYSTEMTIME *time, long *timezone)
|
|
{
|
|
if (!string || (lstrlenW(string) < 10) || !time)
|
|
return FALSE;
|
|
|
|
ZeroMemory(time, sizeof(SYSTEMTIME));
|
|
|
|
if (timezone)
|
|
*timezone = 0;
|
|
|
|
if (IsNumber(string[0]) &&
|
|
IsNumber(string[1]) &&
|
|
IsNumber(string[2]) &&
|
|
IsNumber(string[3]) &&
|
|
(string[4] != L'\0') &&
|
|
IsNumber(string[5]) &&
|
|
IsNumber(string[6]) &&
|
|
(string[7] != L'\0') &&
|
|
IsNumber(string[8]) &&
|
|
IsNumber(string[9]))
|
|
{
|
|
time->wYear = 1000*(string[0] - L'0') +
|
|
100*(string[1] - L'0') +
|
|
10*(string[2] - L'0') +
|
|
string[3] - L'0';
|
|
|
|
time->wMonth = 10*(string[5] - L'0') + string[6] - L'0';
|
|
|
|
time->wDay = 10*(string[8] - L'0') + string[9] - L'0';
|
|
}
|
|
else
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if ((string[10]!= L'\0') &&
|
|
IsNumber(string[11]) &&
|
|
IsNumber(string[12]) &&
|
|
(string[13] != L'\0') &&
|
|
IsNumber(string[14]) &&
|
|
IsNumber(string[15]))
|
|
{
|
|
time->wHour = 10*(string[11] - L'0') + string[12] - L'0';
|
|
time->wMinute = 10*(string[14] - L'0') + string[15] - L'0';
|
|
|
|
if (timezone &&
|
|
(string[16]!= L'\0') &&
|
|
IsNumber(string[17]) &&
|
|
IsNumber(string[18]) &&
|
|
IsNumber(string[19]) &&
|
|
IsNumber(string[20]))
|
|
{
|
|
*timezone = 1000*(string[17] - L'0') +
|
|
100*(string[18] - L'0') +
|
|
10*(string[19] - L'0') +
|
|
string[20] - L'0';
|
|
|
|
if(string[16] == L'-')
|
|
*timezone = - *timezone;
|
|
}
|
|
}
|
|
|
|
return ValidateSystemTime(time);
|
|
}
|
|
|
|
|
|
//==============================================================================
|
|
// CProcessElement class provides generic support for sync or async enumeration
|
|
// of an XML OM
|
|
//==============================================================================
|
|
CProcessElement::CProcessElement(CProcessElementSink *pParent,
|
|
CProcessRoot *pRoot,
|
|
IXMLElement *pEle)
|
|
{
|
|
ASSERT(m_pRunAgent == NULL && m_pCurChild == NULL && m_pCollection == NULL);
|
|
|
|
m_pElement = pEle; pEle->AddRef();
|
|
m_pRoot = pRoot;
|
|
m_pParent = pParent;
|
|
}
|
|
|
|
CProcessElement::~CProcessElement()
|
|
{
|
|
ASSERT(!m_pCurChild);
|
|
|
|
CRunDeliveryAgent::SafeRelease(m_pRunAgent);
|
|
|
|
SAFERELEASE(m_pCollection);
|
|
SAFERELEASE(m_pElement);
|
|
SAFERELEASE(m_pChildElement);
|
|
}
|
|
|
|
HRESULT CProcessElement::Pause(DWORD dwFlags)
|
|
{
|
|
if (m_pCurChild)
|
|
return m_pCurChild->Pause(dwFlags);
|
|
|
|
ASSERT(m_pRunAgent);
|
|
|
|
if (m_pRunAgent)
|
|
return m_pRunAgent->AgentPause(dwFlags);
|
|
|
|
return E_FAIL;
|
|
}
|
|
|
|
HRESULT CProcessElement::Resume(DWORD dwFlags)
|
|
{
|
|
if (m_pCurChild)
|
|
return m_pCurChild->Resume(dwFlags);
|
|
|
|
if (m_pRunAgent)
|
|
m_pRunAgent->AgentResume(dwFlags);
|
|
else
|
|
DoEnumeration();
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT CProcessElement::Abort(DWORD dwFlags)
|
|
{
|
|
if (m_pCurChild)
|
|
{
|
|
m_pCurChild->Abort(dwFlags);
|
|
SAFEDELETE(m_pCurChild);
|
|
}
|
|
if (m_pRunAgent)
|
|
{
|
|
// Prevent reentrancy into OnAgentEnd
|
|
m_pRunAgent->LeaveMeAlone();
|
|
m_pRunAgent->AgentAbort(dwFlags);
|
|
CRunDeliveryAgent::SafeRelease(m_pRunAgent);
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
|
|
HRESULT CProcessElement::Run()
|
|
{
|
|
ASSERT(!m_pCollection);
|
|
ASSERT(m_lMax == 0);
|
|
// ASSERT(m_fSentEnumerationComplete == FALSE); // DoEnumeration may have sent this
|
|
|
|
m_lIndex = 0;
|
|
|
|
if (SUCCEEDED(m_pElement->get_children(&m_pCollection)) && m_pCollection)
|
|
{
|
|
m_pCollection->get_length(&m_lMax);
|
|
}
|
|
else
|
|
m_lMax = 0;
|
|
|
|
return DoEnumeration(); // Will call OnChildDone when appropriate
|
|
}
|
|
|
|
HRESULT CProcessElement::OnAgentEnd(const SUBSCRIPTIONCOOKIE *pSubscriptionCookie,
|
|
long lSizeDownloaded, HRESULT hrResult, LPCWSTR wszResult,
|
|
BOOL fSynchronous)
|
|
{
|
|
// Our delivery agent is done. Continue enumeration
|
|
ASSERT(!m_pCurChild);
|
|
|
|
if (lSizeDownloaded > 0)
|
|
m_pRoot->m_dwCurSizeKB += (ULONG) lSizeDownloaded;
|
|
|
|
TraceMsg(TF_THISMODULE, "ChannelAgent up to %dkb of %dkb", m_pRoot->m_dwCurSizeKB, m_pRoot->m_pChannelAgent->m_dwMaxSizeKB);
|
|
|
|
if ((hrResult == INET_E_AGENT_MAX_SIZE_EXCEEDED) ||
|
|
(hrResult == INET_E_AGENT_CACHE_SIZE_EXCEEDED))
|
|
{
|
|
DBG("CProcessElement got max size or cache size exceeded; not running any more delivery agents");
|
|
|
|
m_pRoot->m_fMaxSizeExceeded = TRUE;
|
|
m_pRoot->m_pChannelAgent->SetEndStatus(hrResult);
|
|
}
|
|
|
|
CRunDeliveryAgent::SafeRelease(m_pRunAgent);
|
|
|
|
if (fSynchronous)
|
|
{
|
|
// we are still in our DoDeliveryAgent call. Let it return out through there.
|
|
return S_OK;
|
|
}
|
|
|
|
// Continue enumeration, or start enumeration if we haven't yet.
|
|
if (m_fStartedEnumeration)
|
|
DoEnumeration();
|
|
else
|
|
Run();
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT CProcessElement::DoEnumeration()
|
|
{
|
|
IDispatch *pDisp;
|
|
IXMLElement *pItem;
|
|
BSTR bstrTagName;
|
|
VARIANT vIndex, vEmpty;
|
|
HRESULT hr = S_OK;
|
|
BOOL fStarted = FALSE;
|
|
|
|
m_fStartedEnumeration = TRUE;
|
|
|
|
ASSERT(m_pCollection || !m_lMax);
|
|
|
|
if (m_pRoot && m_pRoot->IsPaused())
|
|
{
|
|
DBG("CProcessElement::DoEnumeration returning E_PENDING, we're paused");
|
|
return E_PENDING;
|
|
}
|
|
|
|
vEmpty.vt = VT_EMPTY;
|
|
|
|
for (; (m_lIndex < m_lMax) && !fStarted && (hr != E_ABORT); m_lIndex++)
|
|
{
|
|
vIndex.vt = VT_UI4;
|
|
vIndex.lVal = m_lIndex;
|
|
|
|
if (SUCCEEDED(m_pCollection->item(vIndex, vEmpty, &pDisp)))
|
|
{
|
|
if (SUCCEEDED(pDisp->QueryInterface(IID_IXMLElement, (void **)&pItem)))
|
|
{
|
|
if (SUCCEEDED(pItem->get_tagName(&bstrTagName)) && bstrTagName)
|
|
{
|
|
SAFERELEASE(m_pChildElement);
|
|
m_pChildElement=pItem;
|
|
m_pChildElement->AddRef();
|
|
|
|
hr = ProcessItemInEnum(bstrTagName, pItem);
|
|
SysFreeString(bstrTagName);
|
|
if (hr == E_PENDING)
|
|
fStarted = TRUE;
|
|
}
|
|
pItem->Release();
|
|
}
|
|
pDisp->Release();
|
|
}
|
|
}
|
|
|
|
// Tell this instance we're done with enumeration, unless we already have
|
|
if (!fStarted && !m_fSentEnumerationComplete)
|
|
{
|
|
m_fSentEnumerationComplete = TRUE;
|
|
hr = EnumerationComplete(); // WARNING this eats E_ABORT
|
|
if (hr == E_PENDING)
|
|
fStarted = TRUE;
|
|
}
|
|
|
|
// Notify our parent if we're done with our enumeration,
|
|
if (!fStarted)
|
|
{
|
|
if (m_pParent) // Check for CExtractSchedule
|
|
m_pParent->OnChildDone(this, hr); // This may delete us
|
|
}
|
|
|
|
if (hr == E_ABORT)
|
|
return E_ABORT;
|
|
|
|
return (fStarted) ? E_PENDING : S_OK;
|
|
}
|
|
|
|
HRESULT CProcessElement::OnChildDone(CProcessElement *pChild, HRESULT hr)
|
|
{
|
|
ASSERT(pChild && (!m_pCurChild || (pChild == m_pCurChild)));
|
|
|
|
if (m_pCurChild)
|
|
{
|
|
// A child returned from async operation.
|
|
SAFEDELETE(m_pCurChild);
|
|
|
|
// Continue enumeration. This will call our parent's ChildDone if it
|
|
// finishes, so it may delete us.
|
|
DoEnumeration();
|
|
}
|
|
else
|
|
{
|
|
// Our child has finished synchronously. Ignore (DoChild() will take care of it).
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT CProcessElement::DoChild(CProcessElement *pChild)
|
|
{
|
|
HRESULT hr;
|
|
|
|
ASSERT(m_pCurChild == NULL);
|
|
|
|
if (!pChild)
|
|
return E_POINTER; // FEATURE should call parent's OnChildDone here
|
|
|
|
hr = pChild->Run();
|
|
|
|
if (hr == E_PENDING)
|
|
{
|
|
// Returned async. Will call back OnChildDone.
|
|
m_pCurChild = pChild;
|
|
return E_PENDING;
|
|
}
|
|
|
|
// Synchronously returned. Clean up.
|
|
delete pChild;
|
|
|
|
return hr;
|
|
}
|
|
|
|
// E_PENDING if async operation started
|
|
HRESULT CProcessElement::DoDeliveryAgent(ISubscriptionItem *pItem, REFCLSID rclsid, LPCWSTR pwszURL)
|
|
{
|
|
ASSERT(pItem);
|
|
|
|
HRESULT hr=E_FAIL;
|
|
|
|
if (m_pRoot->m_fMaxSizeExceeded)
|
|
{
|
|
// DBG("CProcessElement::RunDeliveryAgent failing; exceeded max size.");
|
|
return E_FAIL;
|
|
}
|
|
|
|
if (m_pRunAgent)
|
|
{
|
|
DBG_WARN("CProcessElement::DoDeliveryAgent already running!");
|
|
return E_FAIL;
|
|
}
|
|
|
|
m_pRunAgent = new CChannelAgentHolder(m_pRoot->m_pChannelAgent, this);
|
|
|
|
if (m_pRunAgent)
|
|
{
|
|
hr = m_pRunAgent->Init(this, pItem, rclsid);
|
|
|
|
if (SUCCEEDED(hr))
|
|
hr = m_pRunAgent->StartAgent();
|
|
|
|
if (hr == E_PENDING)
|
|
{
|
|
m_pRoot->m_pChannelAgent->SendUpdateProgress(pwszURL, ++(m_pRoot->m_iTotalStarted), -1, m_pRoot->m_dwCurSizeKB);
|
|
}
|
|
else
|
|
CRunDeliveryAgent::SafeRelease(m_pRunAgent);
|
|
}
|
|
else
|
|
hr = E_OUTOFMEMORY;
|
|
|
|
return hr;
|
|
}
|
|
|
|
HRESULT CProcessElement::DoSoftDist(IXMLElement *pItem)
|
|
{
|
|
HRESULT hr = S_OK;
|
|
ISubscriptionItem *pSubsItem;
|
|
|
|
if (SUCCEEDED(m_pRoot->CreateStartItem(&pSubsItem)))
|
|
{
|
|
if (pSubsItem)
|
|
{
|
|
hr = DoDeliveryAgent(pSubsItem, CLSID_CDLAgent);
|
|
pSubsItem->Release();
|
|
}
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
HRESULT CProcessElement::DoWebCrawl(IXMLElement *pItem, LPCWSTR pwszURL /* = NULL */)
|
|
{
|
|
BSTR bstrURL=NULL, bstrTmp=NULL;
|
|
HRESULT hr = S_OK;
|
|
ISubscriptionItem *pSubsItem;
|
|
DWORD dwLevels=0, dwFlags;
|
|
LPWSTR pwszUrl2=NULL;
|
|
BOOL fOffline=FALSE;
|
|
|
|
if (!pwszURL && SUCCEEDED(GetXMLStringAttribute(pItem, L"HREF", &bstrURL)) && bstrURL)
|
|
pwszURL = bstrURL;
|
|
|
|
if (pwszURL)
|
|
{
|
|
SYSTEMTIME stLastMod;
|
|
long lTimezone;
|
|
|
|
hr = CombineWithBaseUrl(pwszURL, &pwszUrl2);
|
|
|
|
if (SUCCEEDED(hr) && pwszUrl2)
|
|
pwszURL = pwszUrl2; // Got a new URL
|
|
|
|
hr = CUrlDownload::IsValidURL(pwszURL);
|
|
|
|
if (SUCCEEDED(hr) &&
|
|
SUCCEEDED(GetXMLStringAttribute(m_pElement, L"LastMod", &bstrTmp)) &&
|
|
ISODateToSystemTime(bstrTmp, &stLastMod, &lTimezone))
|
|
{
|
|
// Check Last Modified time
|
|
TCHAR szThisUrl[INTERNET_MAX_URL_LENGTH];
|
|
char chBuf[MY_MAX_CACHE_ENTRY_INFO];
|
|
DWORD dwBufSize = sizeof(chBuf);
|
|
LPINTERNET_CACHE_ENTRY_INFO lpInfo = (LPINTERNET_CACHE_ENTRY_INFO) chBuf;
|
|
|
|
MyOleStrToStrN(szThisUrl, INTERNET_MAX_URL_LENGTH, pwszURL);
|
|
hr = GetUrlInfoAndMakeSticky(NULL, szThisUrl, lpInfo, dwBufSize, 0);
|
|
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
FILETIME ft;
|
|
|
|
if (SystemTimeToFileTime(&stLastMod, &ft))
|
|
{
|
|
// APPCOMPAT: In an ideal world, all servers would support LastModifiedTime accurately.
|
|
// In our world, some do not support it and wininet returns a value of zero.
|
|
// Without maintaining checksums of the files, we have two options: always download
|
|
// the URL or never update it. Since it would be odd not to update it, we always do.
|
|
if ((lpInfo->LastModifiedTime.dwHighDateTime || lpInfo->LastModifiedTime.dwLowDateTime)
|
|
&& (lpInfo->LastModifiedTime.dwHighDateTime >= ft.dwHighDateTime)
|
|
&& ((lpInfo->LastModifiedTime.dwHighDateTime > ft.dwHighDateTime)
|
|
|| (lpInfo->LastModifiedTime.dwLowDateTime >= ft.dwLowDateTime)))
|
|
{
|
|
// Skip it.
|
|
TraceMsg(TF_THISMODULE, "Running webcrawl OFFLINE due to Last Modified time URL=%ws", pwszURL);
|
|
fOffline = TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
hr = S_OK;
|
|
}
|
|
|
|
SAFEFREEBSTR(bstrTmp);
|
|
|
|
if (SUCCEEDED(hr) && SUCCEEDED(m_pRoot->CreateStartItem(&pSubsItem)))
|
|
{
|
|
WriteOLESTR(pSubsItem, c_szPropURL, pwszURL);
|
|
|
|
dwLevels = GetXMLDwordAttribute(pItem, L"LEVEL", 0);
|
|
if (dwLevels && m_pRoot->IsChannelFlagSet(CHANNEL_AGENT_PRECACHE_SOME))
|
|
{
|
|
// Note: MaxChannelLevels is stored as N+1 because 0
|
|
// means the restriction is disabled.
|
|
DWORD dwMaxLevels = SHRestricted2W(REST_MaxChannelLevels, NULL, 0);
|
|
if (!dwMaxLevels)
|
|
dwMaxLevels = MAX_CDF_CRAWL_LEVELS + 1;
|
|
if (dwLevels >= dwMaxLevels)
|
|
dwLevels = dwMaxLevels - 1;
|
|
WriteDWORD(pSubsItem, c_szPropCrawlLevels, dwLevels);
|
|
}
|
|
|
|
if (fOffline)
|
|
{
|
|
if (SUCCEEDED(ReadDWORD(pSubsItem, c_szPropCrawlFlags, &dwFlags)))
|
|
{
|
|
dwFlags |= CWebCrawler::WEBCRAWL_PRIV_OFFLINE_MODE;
|
|
WriteDWORD(pSubsItem, c_szPropCrawlFlags, dwFlags);
|
|
}
|
|
}
|
|
|
|
hr = DoDeliveryAgent(pSubsItem, CLSID_WebCrawlerAgent, pwszURL);
|
|
|
|
SAFERELEASE(pSubsItem);
|
|
}
|
|
}
|
|
|
|
if (bstrURL)
|
|
SysFreeString(bstrURL);
|
|
|
|
if (pwszUrl2)
|
|
MemFree(pwszUrl2);
|
|
|
|
return hr;
|
|
}
|
|
|
|
BOOL CProcessElement::ShouldDownloadLogo(IXMLElement *pLogo)
|
|
{
|
|
return m_pRoot->IsChannelFlagSet(CHANNEL_AGENT_PRECACHE_SOME);
|
|
}
|
|
|
|
// If relative url, will combine with most recent base URL
|
|
// *ppwszRetUrl should be NULL & will be MemAlloced
|
|
HRESULT CProcessElement::CombineWithBaseUrl(LPCWSTR pwszUrl, LPWSTR *ppwszRetUrl)
|
|
{
|
|
ASSERT(ppwszRetUrl && !*ppwszRetUrl && pwszUrl);
|
|
|
|
// Optimization: if pwszURL is absolute, we don't need to do this expensive
|
|
// combine operation
|
|
// if (*pwszUrl != L'/') // BOGUS
|
|
// {
|
|
// *ppwszRetUrl = StrDupW(pwszUrl);
|
|
// return S_FALSE; // Succeeded; pwszUrl is already OK
|
|
// }
|
|
|
|
// Find appropriate Base URL to use
|
|
LPCWSTR pwszBaseUrl = GetBaseUrl();
|
|
|
|
WCHAR wszUrl[INTERNET_MAX_URL_LENGTH];
|
|
DWORD dwLen = ARRAYSIZE(wszUrl);
|
|
|
|
if (SUCCEEDED(UrlCombineW(pwszBaseUrl, pwszUrl, wszUrl, &dwLen, 0)))
|
|
{
|
|
*ppwszRetUrl = StrDupW(wszUrl);
|
|
return (*ppwszRetUrl) ? S_OK : E_OUTOFMEMORY;
|
|
}
|
|
|
|
*ppwszRetUrl = NULL;
|
|
|
|
return E_FAIL; // Erg?
|
|
}
|
|
|
|
|
|
|
|
//==============================================================================
|
|
// CProcessElement derived classes, to handle specific CDF tags
|
|
//==============================================================================
|
|
// CProcessRoot doesn't behave like a normal CProcessElement class. It calls
|
|
// CProcessChannel to process the *same element*
|
|
CProcessRoot::CProcessRoot(CChannelAgent *pParent, IXMLElement *pItem) :
|
|
CProcessElement(pParent, NULL, pItem)
|
|
{
|
|
ASSERT(m_pDefaultStartItem == FALSE && m_pTracking == NULL && !m_dwCurSizeKB);
|
|
|
|
m_pRoot = this;
|
|
m_pChannelAgent = pParent; pParent->AddRef();
|
|
m_iTotalStarted = 1;
|
|
}
|
|
|
|
CProcessRoot::~CProcessRoot()
|
|
{
|
|
SAFEDELETE(m_pTracking);
|
|
SAFERELEASE(m_pChannelAgent);
|
|
SAFERELEASE(m_pDefaultStartItem);
|
|
}
|
|
|
|
// Should never get called. CProcessRoot is an odd duck.
|
|
HRESULT CProcessRoot::ProcessItemInEnum(LPCWSTR pwszTagName, IXMLElement *pItem)
|
|
{
|
|
ASSERT(0);
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
HRESULT CProcessRoot::CreateStartItem(ISubscriptionItem **ppItem)
|
|
{
|
|
if (ppItem)
|
|
*ppItem = NULL;
|
|
|
|
if (!m_pDefaultStartItem)
|
|
{
|
|
DoCloneSubscriptionItem(m_pChannelAgent->GetStartItem(), NULL, &m_pDefaultStartItem);
|
|
|
|
if (m_pDefaultStartItem)
|
|
{
|
|
DWORD dwTemp;
|
|
|
|
// Clear out properties we don't want
|
|
const LPCWSTR pwszPropsToClear[] =
|
|
{
|
|
c_szPropCrawlLevels,
|
|
c_szPropCrawlLocalDest,
|
|
c_szPropCrawlActualSize,
|
|
c_szPropCrawlMaxSize,
|
|
c_szPropCrawlGroupID
|
|
};
|
|
|
|
VARIANT varEmpty[ARRAYSIZE(pwszPropsToClear)] = {0};
|
|
|
|
ASSERT(ARRAYSIZE(pwszPropsToClear) == ARRAYSIZE(varEmpty));
|
|
m_pDefaultStartItem->WriteProperties(
|
|
ARRAYSIZE(pwszPropsToClear), pwszPropsToClear, varEmpty);
|
|
|
|
// Add in properties we do want
|
|
dwTemp = DELIVERY_AGENT_FLAG_NO_BROADCAST |
|
|
DELIVERY_AGENT_FLAG_NO_RESTRICTIONS;
|
|
WriteDWORD(m_pDefaultStartItem, c_szPropAgentFlags, dwTemp);
|
|
if (FAILED(ReadDWORD(m_pDefaultStartItem, c_szPropCrawlFlags, &dwTemp)))
|
|
{
|
|
WriteDWORD(m_pDefaultStartItem, c_szPropCrawlFlags,
|
|
WEBCRAWL_GET_IMAGES|WEBCRAWL_LINKS_ELSEWHERE);
|
|
}
|
|
|
|
WriteLONGLONG(m_pDefaultStartItem, c_szPropCrawlNewGroupID, m_pChannelAgent->m_llCacheGroupID);
|
|
}
|
|
}
|
|
|
|
if (m_pDefaultStartItem && ppItem)
|
|
{
|
|
DoCloneSubscriptionItem(m_pDefaultStartItem, NULL, ppItem);
|
|
|
|
if (*ppItem)
|
|
{
|
|
// Add in properties for our new clone
|
|
if ((m_pChannelAgent->m_dwMaxSizeKB > 0) &&
|
|
(m_dwCurSizeKB <= m_pChannelAgent->m_dwMaxSizeKB))
|
|
|
|
{
|
|
WriteDWORD(*ppItem, c_szPropCrawlMaxSize,
|
|
(m_pChannelAgent->m_dwMaxSizeKB - m_dwCurSizeKB));
|
|
}
|
|
}
|
|
}
|
|
|
|
return (ppItem) ? (*ppItem) ? S_OK : E_FAIL :
|
|
(m_pDefaultStartItem) ? S_OK : E_FAIL;
|
|
}
|
|
|
|
HRESULT CProcessRoot::Run()
|
|
{
|
|
if (FAILED(CreateStartItem(NULL)))
|
|
return E_FAIL;
|
|
|
|
return DoChild(new CProcessChannel(this, this, m_pElement));
|
|
}
|
|
|
|
HRESULT CProcessRoot::DoTrackingFromItem(IXMLElement *pItem, LPCWSTR pwszUrl, BOOL fForceLog)
|
|
{
|
|
HRESULT hr = E_FAIL;
|
|
|
|
// if m_pTracking is not created before this call, means no <LogTarget> tag was found or
|
|
// global logging is turned off
|
|
if (m_pTracking)
|
|
hr = m_pTracking->ProcessTrackingInItem(pItem, pwszUrl, fForceLog);
|
|
|
|
return hr;
|
|
}
|
|
|
|
HRESULT CProcessRoot::DoTrackingFromLog(IXMLElement *pItem)
|
|
{
|
|
HRESULT hr = S_OK;
|
|
|
|
if (!m_pTracking
|
|
&& !SHRestricted2W(REST_NoChannelLogging, m_pChannelAgent->GetUrl(), 0)
|
|
&& !ReadRegDWORD(HKEY_CURRENT_USER, c_szRegKey, c_szNoChannelLogging))
|
|
{
|
|
m_pTracking = new CUrlTrackingCache(m_pChannelAgent->GetStartItem(), m_pChannelAgent->GetUrl());
|
|
}
|
|
|
|
if (!m_pTracking)
|
|
return E_OUTOFMEMORY;
|
|
|
|
hr = m_pTracking->ProcessTrackingInLog(pItem);
|
|
|
|
// skip tracking if PostURL is not specified
|
|
if (m_pTracking->get_PostURL() == NULL)
|
|
{
|
|
SAFEDELETE(m_pTracking);
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
// Overload this since we never do enumeration. Call delivery agent if necessary,
|
|
// call m_pParent->OnChildDone if necessary
|
|
HRESULT CProcessRoot::OnChildDone(CProcessElement *pChild, HRESULT hr)
|
|
{
|
|
ASSERT(pChild && (!m_pCurChild || (pChild == m_pCurChild)));
|
|
|
|
// Our processing is done. Now we decide if we'd like to call the post agent.
|
|
BSTR bstrURL=NULL;
|
|
ISubscriptionItem *pStartItem;
|
|
|
|
hr = S_OK;
|
|
|
|
SAFEDELETE(m_pCurChild);
|
|
|
|
ASSERT(m_pDefaultStartItem);
|
|
ReadBSTR(m_pDefaultStartItem, c_szTrackingPostURL, &bstrURL);
|
|
|
|
if (bstrURL && *bstrURL)
|
|
{
|
|
TraceMsg(TF_THISMODULE, "ChannelAgent calling post agent posturl=%ws", bstrURL);
|
|
if (SUCCEEDED(m_pRoot->CreateStartItem(&pStartItem)))
|
|
{
|
|
m_pRunAgent = new CChannelAgentHolder(m_pChannelAgent, this);
|
|
|
|
if (m_pRunAgent)
|
|
{
|
|
hr = m_pRunAgent->Init(this, pStartItem, CLSID_PostAgent);
|
|
if (SUCCEEDED(hr))
|
|
hr = m_pRunAgent->StartAgent();
|
|
if (hr != E_PENDING)
|
|
CRunDeliveryAgent::SafeRelease(m_pRunAgent);
|
|
}
|
|
pStartItem->Release();
|
|
}
|
|
}
|
|
|
|
SysFreeString(bstrURL);
|
|
|
|
if (hr != E_PENDING)
|
|
m_pParent->OnChildDone(this, hr); // This may delete us
|
|
|
|
return hr;
|
|
}
|
|
|
|
// Our delivery agent (post agent) is done running. Tell CDF agent we're done.
|
|
HRESULT CProcessRoot::OnAgentEnd(const SUBSCRIPTIONCOOKIE *pSubscriptionCookie,
|
|
long lSizeDownloaded, HRESULT hrResult, LPCWSTR wszResult,
|
|
BOOL fSynchronous)
|
|
{
|
|
if (!fSynchronous)
|
|
m_pParent->OnChildDone(this, S_OK); // This may delete us
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
CProcessChannel::CProcessChannel(CProcessElementSink *pParent,
|
|
CProcessRoot *pRoot,
|
|
IXMLElement *pItem) :
|
|
CProcessElement(pParent, pRoot, pItem)
|
|
{
|
|
m_fglobalLog = FALSE;
|
|
}
|
|
|
|
CProcessChannel::~CProcessChannel()
|
|
{
|
|
SAFEFREEBSTR(m_bstrBaseUrl);
|
|
}
|
|
|
|
HRESULT CProcessChannel::CheckPreCache()
|
|
{
|
|
BOOL fPreCache;
|
|
|
|
if (SUCCEEDED(GetXMLBoolAttribute(m_pElement, L"PreCache", &fPreCache)))
|
|
{
|
|
if (fPreCache)
|
|
return S_OK;
|
|
|
|
return S_FALSE;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT CProcessChannel::Run()
|
|
{
|
|
// Process Channel attributes, then any sub elements
|
|
if (0 == m_lIndex)
|
|
{
|
|
m_lIndex ++;
|
|
|
|
BSTR bstrURL=NULL;
|
|
LPWSTR pwszUrl=NULL;
|
|
HRESULT hr = S_OK;
|
|
|
|
ASSERT(!m_bstrBaseUrl);
|
|
|
|
// Get base URL if specified
|
|
GetXMLStringAttribute(m_pElement, L"BASE", &m_bstrBaseUrl);
|
|
|
|
if (SUCCEEDED(GetXMLStringAttribute(m_pElement, L"HREF", &bstrURL)) && bstrURL)
|
|
CombineWithBaseUrl(bstrURL, &pwszUrl);
|
|
|
|
if (pwszUrl && (m_pRoot==m_pParent))
|
|
{
|
|
// Use this as default "email url"
|
|
WriteOLESTR(m_pRoot->m_pChannelAgent->GetStartItem(), c_szPropEmailURL, pwszUrl);
|
|
}
|
|
|
|
if (pwszUrl && m_pRoot->IsChannelFlagSet(CHANNEL_AGENT_PRECACHE_SOME) &&
|
|
(S_OK == CheckPreCache()))
|
|
{
|
|
if (E_PENDING == DoWebCrawl(m_pElement, pwszUrl))
|
|
{
|
|
m_fDownloadedHREF = TRUE;
|
|
hr = E_PENDING;
|
|
}
|
|
|
|
}
|
|
|
|
// If no URL for this <Channel> log, check if global log exists
|
|
if (SUCCEEDED(m_pRoot->DoTrackingFromItem(m_pElement, pwszUrl, m_pParent->IsGlobalLog())))
|
|
{
|
|
SetGlobalLogFlag(TRUE);
|
|
}
|
|
|
|
SAFELOCALFREE(pwszUrl);
|
|
SAFEFREEBSTR(bstrURL);
|
|
|
|
if (hr == E_PENDING)
|
|
return hr;
|
|
}
|
|
|
|
// We've processed attributes. Run sub-elements.
|
|
|
|
return CProcessElement::Run();
|
|
}
|
|
|
|
HRESULT CProcessChannel::ProcessItemInEnum(LPCWSTR pwszTagName, IXMLElement *pItem)
|
|
{
|
|
HRESULT hr;
|
|
BSTR bstrTemp;
|
|
|
|
if (!StrCmpIW(pwszTagName, L"Logo"))
|
|
{
|
|
if (ShouldDownloadLogo(pItem))
|
|
return DoWebCrawl(pItem);
|
|
else
|
|
return S_OK;
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"Item"))
|
|
{
|
|
return DoChild(new CProcessItem(this, m_pRoot, pItem));
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"Channel"))
|
|
{
|
|
return DoChild(new CProcessChannel(this, m_pRoot, pItem));
|
|
}
|
|
/*
|
|
else if (!StrCmpIW(pwszTagName, L"Login"))
|
|
{
|
|
// No sub-elements to process. Do it here.
|
|
return m_pRoot->ProcessLogin(pItem);
|
|
}
|
|
*/
|
|
else if (!StrCmpIW(pwszTagName, L"LOGTARGET"))
|
|
{
|
|
return m_pRoot->DoTrackingFromLog(pItem);
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"Schedule"))
|
|
{
|
|
if (m_pRoot->IsChannelFlagSet(CHANNEL_AGENT_DYNAMIC_SCHEDULE))
|
|
return DoChild(new CProcessSchedule(this, m_pRoot, pItem));
|
|
else
|
|
return S_OK;
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"SoftPkg"))
|
|
{
|
|
return DoSoftDist(pItem);
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"A"))
|
|
{
|
|
// Process Anchor tag
|
|
if (!m_fDownloadedHREF
|
|
&& (m_pRoot->IsChannelFlagSet(CHANNEL_AGENT_PRECACHE_SOME) || (m_pRoot==m_pParent))
|
|
&& SUCCEEDED(GetXMLStringAttribute(pItem, L"HREF", &bstrTemp))
|
|
&& bstrTemp)
|
|
{
|
|
LPWSTR pwszUrl=NULL;
|
|
|
|
hr = S_OK;
|
|
|
|
CombineWithBaseUrl(bstrTemp, &pwszUrl); // not really necessary (a href)
|
|
|
|
if (pwszUrl)
|
|
{
|
|
// Use this as default "email url"
|
|
if (m_pRoot == m_pParent)
|
|
WriteOLESTR(m_pRoot->m_pChannelAgent->GetStartItem(), c_szPropEmailURL, pwszUrl);
|
|
|
|
if (m_pRoot->IsChannelFlagSet(CHANNEL_AGENT_PRECACHE_SOME) &&
|
|
(S_OK == CheckPreCache()))
|
|
{
|
|
hr = DoWebCrawl(m_pElement, pwszUrl);
|
|
|
|
if (E_PENDING == hr)
|
|
m_fDownloadedHREF = TRUE;
|
|
|
|
// Process tracking for this item
|
|
if (SUCCEEDED(m_pRoot->DoTrackingFromItem(m_pElement, pwszUrl, m_pParent->IsGlobalLog())))
|
|
SetGlobalLogFlag(TRUE);
|
|
}
|
|
}
|
|
|
|
SAFELOCALFREE(pwszUrl);
|
|
|
|
SysFreeString(bstrTemp);
|
|
return hr;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
CProcessItem::CProcessItem(CProcessElementSink *pParent,
|
|
CProcessRoot *pRoot,
|
|
IXMLElement *pItem) :
|
|
CProcessElement(pParent, pRoot, pItem)
|
|
{
|
|
}
|
|
|
|
CProcessItem::~CProcessItem()
|
|
{
|
|
SAFEFREEBSTR(m_bstrAnchorURL);
|
|
}
|
|
|
|
HRESULT CProcessItem::ProcessItemInEnum(LPCWSTR pwszTagName, IXMLElement *pItem)
|
|
{
|
|
if (!StrCmpIW(pwszTagName, L"Logo"))
|
|
{
|
|
if (ShouldDownloadLogo(pItem))
|
|
return DoWebCrawl(pItem);
|
|
else
|
|
return S_OK;
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"Usage"))
|
|
{
|
|
// Usage tag found.
|
|
BSTR bstrValue;
|
|
|
|
if (SUCCEEDED(GetXMLStringAttribute(pItem, L"Value", &bstrValue)))
|
|
{
|
|
if (!m_fDesktop &&
|
|
!StrCmpIW(bstrValue, L"DesktopComponent"))
|
|
{
|
|
m_fDesktop = TRUE;
|
|
}
|
|
|
|
if (!m_fEmail &&
|
|
!StrCmpIW(bstrValue, L"Email"))
|
|
{
|
|
m_fEmail = TRUE;
|
|
}
|
|
|
|
SysFreeString(bstrValue);
|
|
}
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"A"))
|
|
{
|
|
// Anchor tag found; save URL
|
|
if (!m_bstrAnchorURL)
|
|
GetXMLStringAttribute(pItem, L"HREF", &m_bstrAnchorURL);
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT CProcessItem::EnumerationComplete()
|
|
{
|
|
BOOL fPreCache, fPreCacheValid=FALSE;
|
|
BOOL fDoDownload=FALSE;
|
|
BSTR bstrURL=NULL;
|
|
HRESULT hr = S_OK;
|
|
LPWSTR pwszUrl=NULL;
|
|
|
|
// End PCN compat
|
|
|
|
if (SUCCEEDED(GetXMLBoolAttribute(m_pElement, L"PreCache", &fPreCache)))
|
|
{
|
|
fPreCacheValid = TRUE;
|
|
}
|
|
|
|
// Get the URL from our attribute, or from Anchor tag if not available
|
|
if (FAILED(GetXMLStringAttribute(m_pElement, L"HREF", &bstrURL)) || !bstrURL)
|
|
{
|
|
bstrURL = m_bstrAnchorURL;
|
|
m_bstrAnchorURL = NULL;
|
|
}
|
|
|
|
// Get the combined URL
|
|
if (bstrURL)
|
|
CombineWithBaseUrl(bstrURL, &pwszUrl);
|
|
|
|
if (pwszUrl)
|
|
{
|
|
// Process tracking for this item
|
|
m_pRoot->DoTrackingFromItem(m_pElement, pwszUrl, IsGlobalLog());
|
|
|
|
// Find if we should use this url for the Email agent
|
|
if (m_fEmail)
|
|
{
|
|
// Yes, put this url in the end report
|
|
DBG("Using custom email url");
|
|
WriteOLESTR(m_pRoot->m_pChannelAgent->GetStartItem(), c_szPropEmailURL, pwszUrl);
|
|
}
|
|
|
|
// Figure out if we should download our "href" based on Usage and Precache tag
|
|
if (fPreCacheValid)
|
|
{
|
|
if (fPreCache)
|
|
{
|
|
if (m_pRoot->IsChannelFlagSet(CHANNEL_AGENT_PRECACHE_SOME))
|
|
fDoDownload = TRUE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (m_pRoot->IsChannelFlagSet(CHANNEL_AGENT_PRECACHE_ALL))
|
|
fDoDownload = TRUE;
|
|
}
|
|
|
|
// if (m_fDesktop)
|
|
// Do something for desktop components
|
|
|
|
if (fDoDownload && pwszUrl)
|
|
hr = DoWebCrawl(m_pElement, pwszUrl);
|
|
} // pwszUrl
|
|
|
|
SAFEFREEBSTR(bstrURL);
|
|
SAFELOCALFREE(pwszUrl);
|
|
|
|
return hr;
|
|
}
|
|
|
|
CProcessSchedule::CProcessSchedule(CProcessElementSink *pParent,
|
|
CProcessRoot *pRoot,
|
|
IXMLElement *pItem) :
|
|
CProcessElement(pParent, pRoot, pItem)
|
|
{
|
|
}
|
|
|
|
HRESULT CProcessSchedule::Run()
|
|
{
|
|
// Get attributes (Start and End date) first
|
|
BSTR bstr=NULL;
|
|
long lTimeZone;
|
|
|
|
if (FAILED(GetXMLStringAttribute(m_pElement, L"StartDate", &bstr)) ||
|
|
!ISODateToSystemTime(bstr, &m_stStartDate, &lTimeZone))
|
|
{
|
|
GetLocalTime(&m_stStartDate);
|
|
}
|
|
SAFEFREEBSTR(bstr);
|
|
|
|
if (FAILED(GetXMLStringAttribute(m_pElement, L"StopDate", &bstr)) ||
|
|
!ISODateToSystemTime(bstr, &m_stEndDate, &lTimeZone))
|
|
{
|
|
ZeroMemory(&m_stEndDate, sizeof(m_stEndDate));
|
|
}
|
|
SAFEFREEBSTR(bstr);
|
|
|
|
return CProcessElement::Run();
|
|
}
|
|
|
|
HRESULT CProcessSchedule::ProcessItemInEnum(LPCWSTR pwszTagName, IXMLElement *pItem)
|
|
{
|
|
if (!StrCmpIW(pwszTagName, L"IntervalTime"))
|
|
{
|
|
GetXMLTimeAttributes(pItem, &m_timeInterval);
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"EarliestTime"))
|
|
{
|
|
GetXMLTimeAttributes(pItem, &m_timeEarliest);
|
|
}
|
|
else if (!StrCmpIW(pwszTagName, L"LatestTime"))
|
|
{
|
|
GetXMLTimeAttributes(pItem, &m_timeLatest);
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT CProcessSchedule::EnumerationComplete()
|
|
{
|
|
DBG("CProcessSchedule::EnumerationComplete");
|
|
|
|
int iZone;
|
|
|
|
if (FAILED(GetXMLTimeZoneAttribute(m_pElement, L"TimeZone", &iZone)))
|
|
iZone = 9999;
|
|
|
|
m_tt.cbTriggerSize = sizeof(m_tt);
|
|
|
|
// m_pRoot is null for XMLElementToTaskTrigger call
|
|
// Always run ScheduleToTaskTrigger
|
|
if (SUCCEEDED(ScheduleToTaskTrigger(&m_tt, &m_stStartDate, &m_stEndDate,
|
|
(long) m_timeInterval.dwConvertedMinutes,
|
|
(long) m_timeEarliest.dwConvertedMinutes,
|
|
(long) m_timeLatest.dwConvertedMinutes,
|
|
iZone))
|
|
&& m_pRoot)
|
|
{
|
|
SUBSCRIPTIONITEMINFO sii = { sizeof(SUBSCRIPTIONITEMINFO) };
|
|
if (SUCCEEDED(m_pRoot->m_pChannelAgent->GetStartItem()->GetSubscriptionItemInfo(&sii)))
|
|
{
|
|
if (sii.ScheduleGroup != GUID_NULL)
|
|
{
|
|
if (FAILED(UpdateScheduleTrigger(&sii.ScheduleGroup, &m_tt)))
|
|
{
|
|
DBG_WARN("Failed to update trigger in publisher's recommended schedule.");
|
|
}
|
|
}
|
|
else
|
|
DBG_WARN("No publisher's recommended schedule in sii");
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT ScheduleToTaskTrigger(TASK_TRIGGER *ptt, SYSTEMTIME *pstStartDate, SYSTEMTIME *pstEndDate,
|
|
long lInterval, long lEarliest, long lLatest, int iZone/*=9999*/)
|
|
{
|
|
// Convert our schedule info to a TASK_TRIGGER struct
|
|
|
|
ASSERT(pstStartDate);
|
|
|
|
int iZoneCorrectionMinutes=0;
|
|
TIME_ZONE_INFORMATION tzi;
|
|
long lRandom;
|
|
|
|
if ((lInterval == 0) ||
|
|
(lInterval > 366 * MINUTES_PER_DAY))
|
|
{
|
|
DBG_WARN("ScheduleToTaskTrigger: Invalid IntervalTime - failing");
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
if (ptt->cbTriggerSize < sizeof(TASK_TRIGGER))
|
|
{
|
|
DBG_WARN("ScheduleToTaskTrigger: ptt->cbTriggerSize not initialized");
|
|
ASSERT(!"ScheduleToTaskTrigger");
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
// Fix any invalid stuff
|
|
if (lInterval < MINUTES_PER_DAY)
|
|
{
|
|
// ROUND so that dwIntervalMinutes is an even divisor of one day
|
|
lInterval = MINUTES_PER_DAY / (MINUTES_PER_DAY / lInterval);
|
|
}
|
|
else
|
|
{
|
|
// ROUND to nearest day
|
|
lInterval = MINUTES_PER_DAY * ((lInterval + 12*60)/MINUTES_PER_DAY);
|
|
}
|
|
if (lEarliest >= lInterval)
|
|
{
|
|
DBG("Invalid EarliestTime specified. Fixing."); // Earliest >= Interval!
|
|
lEarliest = lInterval-1;
|
|
}
|
|
if (lLatest < lEarliest)
|
|
{
|
|
DBG("Invalid LatestTime specified. Fixing."); // Latest < Earliest!
|
|
lLatest = lEarliest;
|
|
}
|
|
if (lLatest-lEarliest > lInterval)
|
|
{
|
|
DBG("Invalid LatestTime specified. Fixing."); // Latest > Interval!
|
|
lLatest = lEarliest+lInterval;
|
|
}
|
|
|
|
lRandom = lLatest - lEarliest;
|
|
ASSERT(lRandom>=0 && lRandom<=lInterval);
|
|
|
|
if (iZone != 9999)
|
|
{
|
|
int iCorrection;
|
|
iCorrection = (60 * (iZone/100)) + (iZone % 100);
|
|
|
|
if (iCorrection < -12*60 || iCorrection > 12*60)
|
|
{
|
|
DBG("ScheduleElementToTaskTrigger: Invalid timezone; ignoring");
|
|
}
|
|
else
|
|
{
|
|
if (TIME_ZONE_ID_INVALID != GetTimeZoneInformation(&tzi))
|
|
{
|
|
// tzi.bias has correction from client timezone to UTC (+8 for US west coast)
|
|
// iCorrection has correction from UTC to server time zone (-5 for US east coast)
|
|
// result is correction from server to client time zone (-3 for east to west coast)
|
|
iZoneCorrectionMinutes = - (iCorrection + tzi.Bias + tzi.StandardBias);
|
|
TraceMsg(TF_THISMODULE, "ServerTimeZone = %d, LocalBias = %d min, RelativeCorrection = %d min", iZone, tzi.Bias+tzi.StandardBias, iZoneCorrectionMinutes);
|
|
}
|
|
else
|
|
{
|
|
DBG_WARN("Unable to get local time zone. Not correcting for time zone.");
|
|
}
|
|
}
|
|
}
|
|
|
|
TraceMsg(TF_THISMODULE, "StartDate = %d/%d/%d StopDate = %d/%d/%d", (int)(pstStartDate->wMonth),(int)(pstStartDate->wDay),(int)(pstStartDate->wYear),(int)(pstEndDate->wMonth),(int)(pstEndDate->wDay),(int)(pstEndDate->wYear));
|
|
TraceMsg(TF_THISMODULE, "IntervalTime = %6d minutes", (int)lInterval);
|
|
TraceMsg(TF_THISMODULE, "EarliestTime = %6d minutes", (int)lEarliest);
|
|
TraceMsg(TF_THISMODULE, "LatestTime = %6d minutes", (int)lLatest);
|
|
TraceMsg(TF_THISMODULE, "RandomTime = %6d minutes", (int)lRandom);
|
|
|
|
if (iZoneCorrectionMinutes != 0)
|
|
{
|
|
if (lInterval % 60)
|
|
{
|
|
DBG("Not correcting for time zone ; interval not multiple of 1 hour");
|
|
}
|
|
else
|
|
{
|
|
// Correct Earliest time for time zone
|
|
lEarliest += (iZoneCorrectionMinutes % lInterval);
|
|
|
|
if (lEarliest < 0)
|
|
lEarliest += lInterval;
|
|
|
|
TraceMsg(TF_THISMODULE, "EarliestTime = %6d minutes (after timezone)", (int)lEarliest);
|
|
}
|
|
}
|
|
|
|
ZeroMemory(ptt, sizeof(*ptt));
|
|
ptt->cbTriggerSize = sizeof(*ptt);
|
|
ptt->wBeginYear = pstStartDate->wYear;
|
|
ptt->wBeginMonth = pstStartDate->wMonth;
|
|
ptt->wBeginDay = pstStartDate->wDay;
|
|
if (pstEndDate && pstEndDate->wYear)
|
|
{
|
|
ptt->rgFlags |= TASK_TRIGGER_FLAG_HAS_END_DATE;
|
|
ptt->wEndYear = pstEndDate->wYear;
|
|
ptt->wEndMonth = pstEndDate->wMonth;
|
|
ptt->wEndDay = pstEndDate->wDay;
|
|
}
|
|
|
|
// Set up Random period ; difference between Latesttime and Earliesttime
|
|
ptt->wRandomMinutesInterval = (WORD) lRandom;
|
|
|
|
ptt->wStartHour = (WORD) (lEarliest / 60);
|
|
ptt->wStartMinute = (WORD) (lEarliest % 60);
|
|
|
|
// Set up according to IntervalTime
|
|
if (lInterval < MINUTES_PER_DAY)
|
|
{
|
|
// Less than one day (1/2 day, 1/3 day, 1/4 day, etc)
|
|
ptt->MinutesDuration = MINUTES_PER_DAY - lEarliest;
|
|
ptt->MinutesInterval = lInterval;
|
|
ptt->TriggerType = TASK_TIME_TRIGGER_DAILY;
|
|
ptt->Type.Daily.DaysInterval = 1;
|
|
}
|
|
else
|
|
{
|
|
// Greater than or equal to one day.
|
|
DWORD dwIntervalDays = lInterval / MINUTES_PER_DAY;
|
|
|
|
TraceMsg(TF_THISMODULE, "Using %d day interval", dwIntervalDays);
|
|
|
|
ptt->TriggerType = TASK_TIME_TRIGGER_DAILY;
|
|
ptt->Type.Daily.DaysInterval = (WORD) dwIntervalDays;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
|
|
//==============================================================================
|
|
// CRunDeliveryAgent provides generic support for synchronous operation of a
|
|
// delivery agent
|
|
// It is aggregatable so that you can add more interfaces to the callback
|
|
//==============================================================================
|
|
CRunDeliveryAgent::CRunDeliveryAgent()
|
|
{
|
|
m_cRef = 1;
|
|
}
|
|
|
|
HRESULT CRunDeliveryAgent::Init(CRunDeliveryAgentSink *pParent,
|
|
ISubscriptionItem *pItem,
|
|
REFCLSID rclsidDest)
|
|
{
|
|
ASSERT(pParent && pItem);
|
|
|
|
if (m_pParent || m_pItem)
|
|
return E_FAIL; // already initialized. can't reuse an instance.
|
|
|
|
if (!pParent || !pItem)
|
|
return E_FAIL;
|
|
|
|
m_pParent = pParent;
|
|
m_clsidDest = rclsidDest;
|
|
|
|
m_pItem = pItem;
|
|
pItem->AddRef();
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
CRunDeliveryAgent::~CRunDeliveryAgent()
|
|
{
|
|
CleanUp();
|
|
}
|
|
|
|
//
|
|
// IUnknown members
|
|
//
|
|
STDMETHODIMP_(ULONG) CRunDeliveryAgent::AddRef(void)
|
|
{
|
|
return ++m_cRef;
|
|
}
|
|
|
|
STDMETHODIMP_(ULONG) CRunDeliveryAgent::Release(void)
|
|
{
|
|
if( 0L != --m_cRef )
|
|
return m_cRef;
|
|
|
|
delete this;
|
|
return 0L;
|
|
}
|
|
|
|
STDMETHODIMP CRunDeliveryAgent::QueryInterface(REFIID riid, void ** ppv)
|
|
{
|
|
*ppv=NULL;
|
|
|
|
// Validate requested interface
|
|
if ((IID_IUnknown == riid) ||
|
|
(IID_ISubscriptionAgentEvents == riid))
|
|
{
|
|
*ppv=(ISubscriptionAgentEvents *)this;
|
|
}
|
|
else
|
|
return E_NOINTERFACE;
|
|
|
|
// Addref through the interface
|
|
((LPUNKNOWN)*ppv)->AddRef();
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
//
|
|
// ISubscriptionAgentEvents members
|
|
//
|
|
STDMETHODIMP CRunDeliveryAgent::UpdateBegin(const SUBSCRIPTIONCOOKIE *)
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP CRunDeliveryAgent::UpdateProgress(
|
|
const SUBSCRIPTIONCOOKIE *,
|
|
long lSizeDownloaded,
|
|
long lProgressCurrent,
|
|
long lProgressMax,
|
|
HRESULT hrStatus,
|
|
LPCWSTR wszStatus)
|
|
{
|
|
if (m_pParent)
|
|
m_pParent->OnAgentProgress();
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP CRunDeliveryAgent::UpdateEnd(const SUBSCRIPTIONCOOKIE *pCookie,
|
|
long lSizeDownloaded,
|
|
HRESULT hrResult,
|
|
LPCWSTR wszResult)
|
|
{
|
|
ASSERT((hrResult != INET_S_AGENT_BASIC_SUCCESS) && (hrResult != E_PENDING));
|
|
|
|
m_hrResult = hrResult;
|
|
if (hrResult == INET_S_AGENT_BASIC_SUCCESS || hrResult == E_PENDING)
|
|
{
|
|
// Shouldn't happen; let's be robust anyway.
|
|
m_hrResult = S_OK;
|
|
}
|
|
|
|
if (m_pParent)
|
|
{
|
|
m_pParent->OnAgentEnd(pCookie, lSizeDownloaded, hrResult, wszResult, m_fInStartAgent);
|
|
}
|
|
|
|
CleanUp();
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP CRunDeliveryAgent::ReportError(
|
|
const SUBSCRIPTIONCOOKIE *pSubscriptionCookie,
|
|
HRESULT hrError,
|
|
LPCWSTR wszError)
|
|
{
|
|
return S_FALSE;
|
|
}
|
|
|
|
HRESULT CRunDeliveryAgent::StartAgent()
|
|
{
|
|
HRESULT hr;
|
|
|
|
if (!m_pParent || !m_pItem || m_pAgent)
|
|
return E_FAIL;
|
|
|
|
AddRef(); // Release before we return from this function
|
|
m_fInStartAgent = TRUE;
|
|
|
|
m_hrResult = INET_S_AGENT_BASIC_SUCCESS;
|
|
|
|
DBG("Using new interfaces to host agent");
|
|
|
|
ASSERT(!m_pAgent);
|
|
|
|
hr = CoCreateInstance(m_clsidDest, NULL, CLSCTX_INPROC_SERVER,
|
|
IID_ISubscriptionAgentControl, (void **)&m_pAgent);
|
|
|
|
if (m_pAgent)
|
|
{
|
|
hr = m_pAgent->StartUpdate(m_pItem, (ISubscriptionAgentEvents *)this);
|
|
}
|
|
|
|
hr = m_hrResult;
|
|
|
|
m_fInStartAgent = FALSE;
|
|
Release();
|
|
|
|
if (hr != INET_S_AGENT_BASIC_SUCCESS)
|
|
{
|
|
return hr;
|
|
}
|
|
|
|
return E_PENDING;
|
|
};
|
|
|
|
HRESULT CRunDeliveryAgent::AgentPause(DWORD dwFlags)
|
|
{
|
|
if (m_pAgent)
|
|
return m_pAgent->PauseUpdate(0);
|
|
|
|
DBG_WARN("CRunDeliveryAgent::AgentPause with no running agent!!");
|
|
return S_FALSE;
|
|
}
|
|
|
|
HRESULT CRunDeliveryAgent::AgentResume(DWORD dwFlags)
|
|
{
|
|
if (m_pAgent)
|
|
return m_pAgent->ResumeUpdate(0);
|
|
|
|
DBG_WARN("CRunDeliveryAgent::AgentResume with no running agent!!");
|
|
|
|
return E_FAIL;
|
|
}
|
|
|
|
HRESULT CRunDeliveryAgent::AgentAbort(DWORD dwFlags)
|
|
{
|
|
if (m_pAgent)
|
|
return m_pAgent->AbortUpdate(0);
|
|
|
|
DBG_WARN("CRunDeliveryAgent::AgentAbort with no running agent!!");
|
|
return S_FALSE;
|
|
}
|
|
|
|
void CRunDeliveryAgent::CleanUp()
|
|
{
|
|
SAFERELEASE(m_pItem);
|
|
SAFERELEASE(m_pAgent);
|
|
m_pParent = NULL;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// CChannelAgentHolder, derives from CRunDeliveryAgent
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////
|
|
CChannelAgentHolder::CChannelAgentHolder(CChannelAgent *pChannelAgent, CProcessElement *pProcess)
|
|
{
|
|
m_pChannelAgent = pChannelAgent;
|
|
m_pProcess = pProcess;
|
|
}
|
|
|
|
CChannelAgentHolder::~CChannelAgentHolder()
|
|
{
|
|
}
|
|
|
|
// Won't compile unless we have addref & release here.
|
|
STDMETHODIMP_(ULONG) CChannelAgentHolder::AddRef(void)
|
|
{
|
|
return CRunDeliveryAgent::AddRef();
|
|
}
|
|
|
|
STDMETHODIMP_(ULONG) CChannelAgentHolder::Release(void)
|
|
{
|
|
return CRunDeliveryAgent::Release();
|
|
}
|
|
|
|
STDMETHODIMP CChannelAgentHolder::QueryInterface(REFIID riid, void ** ppv)
|
|
{
|
|
*ppv=NULL;
|
|
|
|
if (IID_IServiceProvider == riid)
|
|
{
|
|
*ppv = (IServiceProvider *)this;
|
|
}
|
|
else
|
|
return CRunDeliveryAgent::QueryInterface(riid, ppv);
|
|
|
|
// Addref through the interface
|
|
((LPUNKNOWN)*ppv)->AddRef();
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// IQueryService
|
|
// CLSID_ChannelAgent IID_ISubscriptionItem channel agent start item
|
|
// CLSID_XMLDocument IID_IXMLElement current element
|
|
STDMETHODIMP CChannelAgentHolder::QueryService(REFGUID guidService, REFIID riid, void **ppvObject)
|
|
{
|
|
ASSERT(ppvObject);
|
|
if (!ppvObject)
|
|
return E_INVALIDARG;
|
|
|
|
if (!m_pChannelAgent || !m_pProcess || !m_pParent)
|
|
return E_FAIL;
|
|
|
|
*ppvObject = NULL;
|
|
|
|
if (guidService == CLSID_ChannelAgent)
|
|
{
|
|
if (riid == IID_ISubscriptionItem)
|
|
{
|
|
*ppvObject = m_pChannelAgent->GetStartItem();
|
|
}
|
|
// if (riid == IID_IXMLElement) Root XML document?
|
|
}
|
|
else if (guidService == CLSID_XMLDocument)
|
|
{
|
|
if (riid == IID_IXMLElement)
|
|
{
|
|
*ppvObject = m_pProcess->GetCurrentElement();
|
|
}
|
|
}
|
|
|
|
if (*ppvObject)
|
|
{
|
|
((IUnknown *)*ppvObject)->AddRef();
|
|
return S_OK;
|
|
}
|
|
|
|
return E_FAIL;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// CChannelAgent implementation
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
CChannelAgent::CChannelAgent()
|
|
{
|
|
DBG("Creating CChannelAgent object");
|
|
|
|
// Initialize object
|
|
// Many vars are initialized in StartOperation
|
|
m_pwszURL = NULL;
|
|
m_pCurDownload = NULL;
|
|
m_pProcess = NULL;
|
|
m_fHasInitCookie = FALSE;
|
|
m_pChannelIconHelper = NULL;
|
|
}
|
|
|
|
CChannelAgent::~CChannelAgent()
|
|
{
|
|
// DBG("Destroying CChannelAgent object");
|
|
|
|
if (m_pwszURL)
|
|
CoTaskMemFree(m_pwszURL);
|
|
|
|
SAFELOCALFREE (m_pBuf);
|
|
|
|
ASSERT(!m_pProcess);
|
|
|
|
SAFERELEASE(m_pChannelIconHelper);
|
|
|
|
DBG("Destroyed CChannelAgent object");
|
|
}
|
|
|
|
void CChannelAgent::CleanUp()
|
|
{
|
|
if (m_pCurDownload)
|
|
{
|
|
m_pCurDownload->LeaveMeAlone(); // no more calls from them
|
|
m_pCurDownload->DoneDownloading();
|
|
m_pCurDownload->Release();
|
|
m_pCurDownload = NULL;
|
|
}
|
|
SAFEFREEOLESTR(m_pwszURL);
|
|
SAFEDELETE(m_pProcess);
|
|
SAFELOCALFREE(m_pBuf);
|
|
|
|
CDeliveryAgent::CleanUp();
|
|
}
|
|
|
|
HRESULT CChannelAgent::StartOperation()
|
|
{
|
|
DBG("Channel Agent in StartOperation");
|
|
|
|
DWORD dwTemp;
|
|
|
|
SAFEFREEOLESTR(m_pwszURL);
|
|
if (FAILED(ReadOLESTR(m_pSubscriptionItem, c_szPropURL, &m_pwszURL)) ||
|
|
!CUrlDownload::IsValidURL(m_pwszURL))
|
|
{
|
|
DBG_WARN("Couldn't get valid URL, aborting");
|
|
SetEndStatus(E_INVALIDARG);
|
|
SendUpdateNone();
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
if (FAILED(ReadDWORD(m_pSubscriptionItem, c_szPropChannelFlags, &m_dwChannelFlags)))
|
|
m_dwChannelFlags = 0;
|
|
|
|
// If we download all, we also download some. Makes assumptions easier.
|
|
if (m_dwChannelFlags & CHANNEL_AGENT_PRECACHE_ALL)
|
|
m_dwChannelFlags |= CHANNEL_AGENT_PRECACHE_SOME;
|
|
|
|
// NOTE: We may want REST_NoChannelContent to be similar to the webcrawl version.
|
|
// Probably not though because the headlines are useful in the UI.
|
|
if (SHRestricted2W(REST_NoChannelContent, NULL, 0))
|
|
ClearFlag(m_dwChannelFlags, CHANNEL_AGENT_PRECACHE_ALL | CHANNEL_AGENT_PRECACHE_SOME);
|
|
|
|
m_dwMaxSizeKB = SHRestricted2W(REST_MaxChannelSize, NULL, 0);
|
|
if (SUCCEEDED(ReadDWORD(m_pSubscriptionItem, c_szPropCrawlMaxSize, &dwTemp))
|
|
&& dwTemp
|
|
&& (0 == m_dwMaxSizeKB || dwTemp < m_dwMaxSizeKB))
|
|
{
|
|
m_dwMaxSizeKB = dwTemp;
|
|
}
|
|
|
|
if (IsAgentFlagSet(FLAG_CHANGESONLY))
|
|
{
|
|
ClearFlag(m_dwChannelFlags, CHANNEL_AGENT_PRECACHE_ALL|
|
|
CHANNEL_AGENT_PRECACHE_SOME|CHANNEL_AGENT_PRECACHE_SCRNSAVER);
|
|
DBG("Channel agent is in 'changes only' mode.");
|
|
}
|
|
else
|
|
{
|
|
// Read old group ID
|
|
ReadLONGLONG(m_pSubscriptionItem, c_szPropCrawlGroupID, &m_llOldCacheGroupID);
|
|
|
|
// Read new ID if present
|
|
m_llCacheGroupID = 0;
|
|
ReadLONGLONG(m_pSubscriptionItem, c_szPropCrawlNewGroupID, &m_llCacheGroupID);
|
|
}
|
|
|
|
return CDeliveryAgent::StartOperation();
|
|
}
|
|
|
|
HRESULT CChannelAgent::StartDownload()
|
|
{
|
|
ASSERT(!m_pCurDownload);
|
|
TraceMsg(TF_THISMODULE, "Channel agent starting download of CDF: URL=%ws", m_pwszURL);
|
|
|
|
m_pCurDownload = new CUrlDownload(this, 0);
|
|
if (!m_pCurDownload)
|
|
return E_OUTOFMEMORY;
|
|
|
|
// Change detection
|
|
m_varChange.vt = VT_EMPTY;
|
|
if (IsAgentFlagSet(FLAG_CHANGESONLY))
|
|
{
|
|
// "Changes Only" mode, we have persisted a change detection code
|
|
ReadVariant(m_pSubscriptionItem, c_szPropChangeCode, &m_varChange);
|
|
m_llCacheGroupID = 0;
|
|
}
|
|
else
|
|
{
|
|
// Create new cache group
|
|
if (!m_llCacheGroupID)
|
|
{
|
|
m_llCacheGroupID = CreateUrlCacheGroup(CACHEGROUP_FLAG_NONPURGEABLE, 0);
|
|
|
|
ASSERT_MSG(m_llCacheGroupID != 0, "Create cache group failed");
|
|
}
|
|
}
|
|
|
|
TCHAR szUrl[INTERNET_MAX_URL_LENGTH];
|
|
|
|
MyOleStrToStrN(szUrl, INTERNET_MAX_URL_LENGTH, m_pwszURL);
|
|
PreCheckUrlForChange(szUrl, &m_varChange, NULL);
|
|
|
|
SendUpdateProgress(m_pwszURL, 0, -1, 0);
|
|
|
|
// Start download
|
|
return m_pCurDownload->BeginDownloadURL2(
|
|
m_pwszURL, BDU2_URLMON, BDU2_NEEDSTREAM, NULL, m_dwMaxSizeKB<<10);
|
|
}
|
|
|
|
HRESULT CChannelAgent::OnAuthenticate(HWND *phwnd, LPWSTR *ppszUsername, LPWSTR *ppszPassword)
|
|
{
|
|
HRESULT hr;
|
|
ASSERT(phwnd && ppszUsername && ppszPassword);
|
|
ASSERT((HWND)-1 == *phwnd && NULL == *ppszUsername && NULL == *ppszPassword);
|
|
|
|
hr = ReadOLESTR(m_pSubscriptionItem, c_szPropCrawlUsername, ppszUsername);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
BSTR bstrPassword = NULL;
|
|
hr = ReadPassword(m_pSubscriptionItem, &bstrPassword);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
int len = (lstrlenW(bstrPassword) + 1) * sizeof(WCHAR);
|
|
*ppszPassword = (LPWSTR) CoTaskMemAlloc(len);
|
|
if (*ppszPassword)
|
|
{
|
|
CopyMemory(*ppszPassword, bstrPassword, len);
|
|
}
|
|
SAFEFREEBSTR(bstrPassword);
|
|
if (*ppszPassword)
|
|
{
|
|
return S_OK;
|
|
}
|
|
}
|
|
}
|
|
|
|
SAFEFREEOLESTR(*ppszUsername);
|
|
SAFEFREEOLESTR(*ppszPassword);
|
|
return E_FAIL;
|
|
}
|
|
|
|
HRESULT CChannelAgent::OnDownloadComplete(UINT iID, int iError)
|
|
{
|
|
TraceMsg(TF_THISMODULE, "Channel Agent: OnDownloadComplete(%d)", iError);
|
|
|
|
IStream *pStm = NULL;
|
|
HRESULT hr;
|
|
BOOL fProcessed=FALSE;
|
|
DWORD dwCDFSizeKB=0, dwResponseCode;
|
|
BSTR bstrTmp;
|
|
char chBuf[MY_MAX_CACHE_ENTRY_INFO];
|
|
DWORD dwBufSize = sizeof(chBuf);
|
|
|
|
LPINTERNET_CACHE_ENTRY_INFO lpInfo = (LPINTERNET_CACHE_ENTRY_INFO) chBuf;
|
|
|
|
if (iError)
|
|
hr = E_FAIL;
|
|
else
|
|
{
|
|
hr = m_pCurDownload->GetResponseCode(&dwResponseCode);
|
|
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
hr = CheckResponseCode(dwResponseCode);
|
|
}
|
|
else
|
|
DBG_WARN("CChannelAgent failed to GetResponseCode");
|
|
}
|
|
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
hr = m_pCurDownload->GetStream(&pStm);
|
|
m_pCurDownload->ReleaseStream();
|
|
}
|
|
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
TCHAR szThisUrl[INTERNET_MAX_URL_LENGTH];
|
|
LPWSTR pwszThisUrl;
|
|
|
|
m_pCurDownload->GetRealURL(&pwszThisUrl);
|
|
|
|
if (pwszThisUrl)
|
|
{
|
|
MyOleStrToStrN(szThisUrl, INTERNET_MAX_URL_LENGTH, pwszThisUrl);
|
|
|
|
LocalFree(pwszThisUrl);
|
|
|
|
if (SUCCEEDED(GetUrlInfoAndMakeSticky(
|
|
NULL,
|
|
szThisUrl,
|
|
lpInfo,
|
|
dwBufSize,
|
|
m_llCacheGroupID)))
|
|
{
|
|
dwCDFSizeKB = (((LPINTERNET_CACHE_ENTRY_INFO)chBuf)->dwSizeLow+512) >> 10;
|
|
TraceMsg(TF_THISMODULE, "CDF size %d kb", dwCDFSizeKB);
|
|
|
|
hr = PostCheckUrlForChange(&m_varChange, lpInfo, lpInfo->LastModifiedTime);
|
|
// If we FAILED, we mark it as changed.
|
|
if (hr == S_OK || FAILED(hr))
|
|
{
|
|
SetAgentFlag(FLAG_CDFCHANGED);
|
|
DBG("CDF has changed; will flag channel as changed");
|
|
}
|
|
|
|
// "Changes Only" mode, persist change detection code
|
|
if (IsAgentFlagSet(FLAG_CHANGESONLY))
|
|
{
|
|
WriteVariant(m_pSubscriptionItem, c_szPropChangeCode, &m_varChange);
|
|
}
|
|
|
|
hr = S_OK;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetEndStatus(E_INVALIDARG);
|
|
}
|
|
|
|
// Get an object model on our Channel Description File
|
|
if (SUCCEEDED(hr) && pStm)
|
|
{
|
|
IPersistStreamInit *pPersistStm=NULL;
|
|
|
|
CoCreateInstance(CLSID_XMLDocument, NULL, CLSCTX_INPROC,
|
|
IID_IPersistStreamInit, (void **)&pPersistStm);
|
|
|
|
if (pPersistStm)
|
|
{
|
|
pPersistStm->InitNew();
|
|
hr = pPersistStm->Load(pStm);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
IXMLDocument *pDoc;
|
|
|
|
hr = pPersistStm->QueryInterface(IID_IXMLDocument, (void **)&pDoc);
|
|
if (SUCCEEDED(hr) && pDoc)
|
|
{
|
|
IXMLElement *pRoot;
|
|
BSTR bstrCharSet=NULL;
|
|
|
|
if (SUCCEEDED(pDoc->get_charset(&bstrCharSet)) && bstrCharSet)
|
|
{
|
|
WriteOLESTR(m_pSubscriptionItem, c_szPropCharSet, bstrCharSet);
|
|
TraceMsg(TF_THISMODULE, "Charset = \"%ws\"", bstrCharSet);
|
|
SysFreeString(bstrCharSet);
|
|
}
|
|
else
|
|
WriteEMPTY(m_pSubscriptionItem, c_szPropCharSet);
|
|
|
|
hr = pDoc->get_root(&pRoot);
|
|
if (SUCCEEDED(hr) && pRoot)
|
|
{
|
|
if (SUCCEEDED(pRoot->get_tagName(&bstrTmp)) && bstrTmp)
|
|
{
|
|
if (!StrCmpIW(bstrTmp, L"Channel"))
|
|
{
|
|
ASSERT(!m_pProcess);
|
|
m_pProcess = new CProcessRoot(this, pRoot);
|
|
if (m_pProcess)
|
|
{
|
|
if (IsAgentFlagSet(FLAG_CDFCHANGED))
|
|
SetEndStatus(S_OK);
|
|
else
|
|
SetEndStatus(S_FALSE);
|
|
|
|
m_pProcess->m_dwCurSizeKB = dwCDFSizeKB;
|
|
WriteEMPTY(m_pSubscriptionItem, c_szPropEmailURL);
|
|
|
|
hr = m_pProcess->Run(); // This will get us cleaned up (now or later)
|
|
fProcessed = TRUE; // So we shouldn't do it ourselves
|
|
}
|
|
}
|
|
else
|
|
DBG_WARN("Valid XML but invalid CDF");
|
|
|
|
SAFEFREEBSTR(bstrTmp);
|
|
}
|
|
pRoot->Release();
|
|
}
|
|
pDoc->Release();
|
|
}
|
|
}
|
|
pPersistStm->Release();
|
|
}
|
|
}
|
|
|
|
if (!fProcessed || (FAILED(hr) && (hr != E_PENDING)))
|
|
{
|
|
if (INET_S_AGENT_BASIC_SUCCESS == GetEndStatus())
|
|
SetEndStatus(E_FAIL);
|
|
DBG_WARN("Failed to process CDF ; XML load failed?");
|
|
CleanUp(); // CleanUp only if the process failed (otherwise OnChildDone does it)
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
if (hr == E_PENDING)
|
|
DBG("CChannelAgent::OnDownloadComplete not cleaning up, webcrawl pending");
|
|
#endif
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT CChannelAgent::OnChildDone(CProcessElement *pChild, HRESULT hr)
|
|
{
|
|
// Our CProcessRoot has reported that it's done. Clean up.
|
|
DBG("CChannelAgent::OnChildDone cleaning up Channel delivery agent");
|
|
|
|
if (m_llOldCacheGroupID)
|
|
{
|
|
DBG("Nuking old cache group.");
|
|
if (!DeleteUrlCacheGroup(m_llOldCacheGroupID, 0, 0))
|
|
{
|
|
DBG_WARN("Failed to delete old cache group!");
|
|
}
|
|
}
|
|
|
|
WriteLONGLONG(m_pSubscriptionItem, c_szPropCrawlGroupID, m_llCacheGroupID);
|
|
|
|
// Add "total size" property
|
|
m_lSizeDownloadedKB = (long) (m_pProcess->m_dwCurSizeKB);
|
|
WriteDWORD(m_pSubscriptionItem, c_szPropCrawlActualSize, m_lSizeDownloadedKB);
|
|
|
|
WriteDWORD(m_pSubscriptionItem, c_szPropActualProgressMax, m_pProcess->m_iTotalStarted);
|
|
|
|
CleanUp();
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT CChannelAgent::AgentPause(DWORD dwFlags)
|
|
{
|
|
DBG("CChannelAgent::AgentPause");
|
|
|
|
if (m_pProcess)
|
|
m_pProcess->Pause(dwFlags);
|
|
|
|
return CDeliveryAgent::AgentPause(dwFlags);
|
|
}
|
|
|
|
HRESULT CChannelAgent::AgentResume(DWORD dwFlags)
|
|
{
|
|
DBG("CChannelAgent::AgentResume");
|
|
|
|
if (m_pProcess)
|
|
m_pProcess->Resume(dwFlags);
|
|
|
|
return CDeliveryAgent::AgentResume(dwFlags);
|
|
}
|
|
|
|
// Forcibly abort current operation
|
|
HRESULT CChannelAgent::AgentAbort(DWORD dwFlags)
|
|
{
|
|
DBG("CChannelAgent::AgentAbort");
|
|
|
|
if (m_pCurDownload)
|
|
m_pCurDownload->DoneDownloading();
|
|
|
|
if (m_pProcess)
|
|
m_pProcess->Abort(dwFlags);
|
|
|
|
return CDeliveryAgent::AgentAbort(dwFlags);
|
|
}
|
|
|
|
HRESULT CChannelAgent::ModifyUpdateEnd(ISubscriptionItem *pEndItem, UINT *puiRes)
|
|
{
|
|
// Customize our end status string
|
|
switch (GetEndStatus())
|
|
{
|
|
case INET_E_AGENT_MAX_SIZE_EXCEEDED :
|
|
*puiRes = IDS_AGNT_STATUS_SIZELIMIT; break;
|
|
case INET_E_AGENT_CACHE_SIZE_EXCEEDED :
|
|
*puiRes = IDS_AGNT_STATUS_CACHELIMIT; break;
|
|
case E_FAIL : *puiRes = IDS_CRAWL_STATUS_NOT_OK; break;
|
|
case S_OK :
|
|
if (!IsAgentFlagSet(FLAG_CHANGESONLY))
|
|
*puiRes = IDS_CRAWL_STATUS_OK;
|
|
else
|
|
*puiRes = IDS_URL_STATUS_OK;
|
|
break;
|
|
case S_FALSE :
|
|
if (!IsAgentFlagSet(FLAG_CHANGESONLY))
|
|
*puiRes = IDS_CRAWL_STATUS_UNCHANGED;
|
|
else
|
|
*puiRes = IDS_URL_STATUS_UNCHANGED;
|
|
break;
|
|
case INET_S_AGENT_PART_FAIL : *puiRes = IDS_CRAWL_STATUS_MOSTLYOK; break;
|
|
}
|
|
|
|
return CDeliveryAgent::ModifyUpdateEnd(pEndItem, puiRes);
|
|
}
|
|
|
|
|
|
const GUID CLSID_CDFICONHANDLER =
|
|
{0xf3ba0dc0, 0x9cc8, 0x11d0, {0xa5, 0x99, 0x0, 0xc0, 0x4f, 0xd6, 0x44, 0x35}};
|
|
|
|
extern HRESULT LoadWithCookie(LPCTSTR, POOEBuf, DWORD *, SUBSCRIPTIONCOOKIE *);
|
|
|
|
// IExtractIcon members
|
|
STDMETHODIMP CChannelAgent::GetIconLocation(UINT uFlags, LPTSTR szIconFile, UINT cchMax, int * piIndex, UINT * pwFlags)
|
|
{
|
|
DWORD dwSize;
|
|
IChannelMgrPriv* pIChannelMgrPriv = NULL;
|
|
HRESULT hr = E_FAIL;
|
|
TCHAR szPath[MAX_PATH];
|
|
|
|
if (!m_pBuf) {
|
|
m_pBuf = (POOEBuf)MemAlloc(LPTR, sizeof(OOEBuf));
|
|
if (!m_pBuf)
|
|
return E_OUTOFMEMORY;
|
|
|
|
HRESULT hr = LoadWithCookie(NULL, m_pBuf, &dwSize, &m_SubscriptionCookie);
|
|
RETURN_ON_FAILURE(hr);
|
|
}
|
|
|
|
hr = GetChannelPath(m_pBuf->m_URL, szPath, ARRAYSIZE(szPath), &pIChannelMgrPriv);
|
|
|
|
if (SUCCEEDED(hr) && pIChannelMgrPriv)
|
|
{
|
|
IPersistFile* ppf = NULL;
|
|
BOOL bCoinit = FALSE;
|
|
HRESULT hr2 = E_FAIL;
|
|
|
|
pIChannelMgrPriv->Release();
|
|
|
|
hr = CoCreateInstance(CLSID_CDFICONHANDLER, NULL, CLSCTX_INPROC_SERVER,
|
|
IID_IPersistFile, (void**)&ppf);
|
|
|
|
if ((hr == CO_E_NOTINITIALIZED || hr == REGDB_E_IIDNOTREG) &&
|
|
SUCCEEDED(CoInitialize(NULL)))
|
|
{
|
|
bCoinit = TRUE;
|
|
hr = CoCreateInstance(CLSID_CDFICONHANDLER, NULL, CLSCTX_INPROC_SERVER,
|
|
IID_IPersistFile, (void**)&ppf);
|
|
}
|
|
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
|
|
hr = ppf->QueryInterface(IID_IExtractIcon, (void**)&m_pChannelIconHelper);
|
|
|
|
WCHAR wszPath[MAX_PATH];
|
|
MyStrToOleStrN(wszPath, ARRAYSIZE(wszPath), szPath);
|
|
hr2 = ppf->Load(wszPath, 0);
|
|
|
|
ppf->Release();
|
|
}
|
|
|
|
if (SUCCEEDED(hr) && m_pChannelIconHelper)
|
|
{
|
|
hr = m_pChannelIconHelper->GetIconLocation(uFlags, szIconFile, cchMax, piIndex, pwFlags);
|
|
}
|
|
|
|
if (bCoinit)
|
|
CoUninitialize();
|
|
|
|
}
|
|
|
|
if (m_pChannelIconHelper == NULL)
|
|
{
|
|
WCHAR wszCookie[GUIDSTR_MAX];
|
|
|
|
ASSERT (piIndex && pwFlags && szIconFile);
|
|
|
|
StringFromGUID2(m_SubscriptionCookie, wszCookie, ARRAYSIZE(wszCookie));
|
|
MyOleStrToStrN(szIconFile, cchMax, wszCookie);
|
|
*piIndex = 0;
|
|
*pwFlags |= GIL_NOTFILENAME | GIL_PERINSTANCE;
|
|
hr = NOERROR;
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
STDMETHODIMP CChannelAgent::Extract(LPCTSTR szIconFile, UINT nIconIndex, HICON * phiconLarge, HICON * phiconSmall, UINT nIconSize)
|
|
{
|
|
static HICON channelIcon = NULL;
|
|
|
|
if (!phiconLarge || !phiconSmall)
|
|
return E_INVALIDARG;
|
|
|
|
* phiconLarge = * phiconSmall = NULL;
|
|
|
|
if (m_pChannelIconHelper)
|
|
{
|
|
return m_pChannelIconHelper->Extract(szIconFile, nIconIndex, phiconLarge, phiconSmall, nIconSize);
|
|
}
|
|
else
|
|
{
|
|
DWORD dwSize;
|
|
|
|
if (!m_pBuf) {
|
|
m_pBuf = (POOEBuf)MemAlloc(LPTR, sizeof(OOEBuf));
|
|
if (!m_pBuf)
|
|
return E_OUTOFMEMORY;
|
|
|
|
HRESULT hr = LoadWithCookie(NULL, m_pBuf, &dwSize, &m_SubscriptionCookie);
|
|
RETURN_ON_FAILURE(hr);
|
|
}
|
|
|
|
BYTE bBuf[MY_MAX_CACHE_ENTRY_INFO];
|
|
LPINTERNET_CACHE_ENTRY_INFO pEntry = (INTERNET_CACHE_ENTRY_INFO *)bBuf;
|
|
|
|
dwSize = sizeof(bBuf);
|
|
if (GetUrlCacheEntryInfo(m_pBuf->m_URL, pEntry, &dwSize)) {
|
|
SHFILEINFO sfi;
|
|
UINT cbFileInfo = sizeof(sfi), uFlags = SHGFI_ICON | SHGFI_LARGEICON;
|
|
|
|
if (NULL != SHGetFileInfo(pEntry->lpszLocalFileName, 0,
|
|
&sfi, cbFileInfo, uFlags))
|
|
{
|
|
ASSERT(sfi.hIcon);
|
|
*phiconLarge = *phiconSmall = sfi.hIcon;
|
|
return NOERROR;
|
|
}
|
|
}
|
|
|
|
if (channelIcon == NULL) {
|
|
channelIcon = LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_CHANNEL));
|
|
ASSERT(channelIcon);
|
|
}
|
|
|
|
* phiconLarge = * phiconSmall = channelIcon;
|
|
return NOERROR;
|
|
}
|
|
}
|