// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. #include "precomp.h" // Constants LPCWSTR vcsEulaQuery = L"SELECT `Text` FROM `Control` WHERE `Control`='LicenseText'"; enum eEulaQuery { eqText = 1}; const int IDM_POPULATE = 100; const int IDM_PRINT = 101; const int CONTROL_X_COORDINATE = 0; const int CONTROL_Y_COORDINATE = 0; const int CONTROL_WIDTH = 500; const int CONTROL_HEIGHT = 500; const int ONE_INCH = 1440; // 1440 TWIPS = 1 inch. const int TEXT_RECORD_POS = 1; const int STRING_CAPACITY = 512; const int NO_OF_COPIES = 1; const LPWSTR WINDOW_CLASS = L"PrintEulaRichText"; //Forward declarations of functions, check the function definitions for the comments static LRESULT CALLBACK WndProc(__in HWND hWnd, __in UINT message, __in WPARAM wParam, __in LPARAM lParam); static HRESULT ReadEulaText(__in MSIHANDLE hInstall, __out LPSTR* ppszEulaText); static DWORD CALLBACK ReadStreamCallback(__in DWORD Cookie, __out LPBYTE pbBuff, __in LONG cb, __out LONG FAR *pcb); static HRESULT CreateRichTextWindow(__out HWND* phWndMain, __out BOOL* pfRegisteredClass); static HRESULT PrintRichText(__in HWND hWndMain); static void Print(__in_opt HWND hWnd); static void LoadEulaText(__in_opt HWND hWnd); static void ShowErrorMessage(__in HRESULT hr); //Global variables PRINTDLGEXW* vpPrintDlg = NULL; //Parameters for print (needed on both sides of WndProc callbacks) LPSTR vpszEulaText = NULL; LPCWSTR vwzRichEditClass = NULL; HRESULT vhr = S_OK; //Global hr, used by the functions called from WndProc to set errorcode /******************************************************************** PrintEula - Custom Action entry point ********************************************************************/ extern "C" UINT __stdcall PrintEula(MSIHANDLE hInstall) { //AssertSz(FALSE, "Debug PrintEula"); HRESULT hr = S_OK; HWND hWndMain = NULL; HMODULE hRichEdit = NULL; BOOL fRegisteredClass = FALSE; hr = WcaInitialize(hInstall, "PrintEula"); ExitOnFailure(hr, "failed to initialize"); // Initialize then display print dialog. vpPrintDlg = (PRINTDLGEXW*)GlobalAlloc(GPTR, sizeof(PRINTDLGEXW)); // MSDN says to allocate on heap. ExitOnNullWithLastError(vpPrintDlg, hr, "Failed to allocate memory for print dialog struct."); vpPrintDlg->lStructSize = sizeof(PRINTDLGEX); vpPrintDlg->hwndOwner = ::FindWindowW(L"MsiDialogCloseClass", NULL); vpPrintDlg->Flags = PD_RETURNDC | PD_COLLATE | PD_NOCURRENTPAGE | PD_ALLPAGES | PD_NOPAGENUMS | PD_NOSELECTION; vpPrintDlg->nCopies = NO_OF_COPIES; vpPrintDlg->nStartPage = START_PAGE_GENERAL; hr = ::PrintDlgExW(vpPrintDlg); ExitOnFailure(hr, "Failed to show print dialog"); // If user said they want to print. if (PD_RESULT_PRINT == vpPrintDlg->dwResultAction) { // Get the stream for Eula hr = ReadEulaText(hInstall, &vpszEulaText); ExitOnFailure(hr, "failed to read Eula text from MSI database"); // Have to load Rich Edit since we'll be creating a Rich Edit control in the window hr = LoadSystemLibrary(L"Msftedit.dll", &hRichEdit); if (SUCCEEDED(hr)) { vwzRichEditClass = MSFTEDIT_CLASS; } else { hr = LoadSystemLibrary(L"Riched20.dll", &hRichEdit); ExitOnFailure(hr, "failed to load rich edit 2.0 library"); vwzRichEditClass = RICHEDIT_CLASSW; } hr = CreateRichTextWindow(&hWndMain, &fRegisteredClass); ExitOnFailure(hr, "failed to create rich text window for printing"); hr = PrintRichText(hWndMain); if (FAILED(hr)) // Since we've already shown the print dialog, we better show them a dialog explaining why it didn't print { ShowErrorMessage(hr); } } LExit: ReleaseNullStr(vpszEulaText); if (vpPrintDlg) { if (vpPrintDlg->hDevMode) { ::GlobalFree(vpPrintDlg->hDevMode); } if (vpPrintDlg->hDevNames) { ::GlobalFree(vpPrintDlg->hDevNames); } if (vpPrintDlg->hDC) { ::DeleteDC(vpPrintDlg->hDC); } ::GlobalFree(vpPrintDlg); vpPrintDlg = NULL; } if (fRegisteredClass) { ::UnregisterClassW(WINDOW_CLASS, NULL); } vwzRichEditClass = NULL; if (NULL != hRichEdit) { ::FreeLibrary(hRichEdit); } // Always return success since we dont want to stop the // installation even if the Eula printing fails. return WcaFinalize(ERROR_SUCCESS); } /******************************************************************** CreateRichTextWindow - Creates Window and Child RichText control. ********************************************************************/ HRESULT CreateRichTextWindow( __out HWND* phWndMain, __out BOOL* pfRegisteredClass ) { HRESULT hr = S_OK; HWND hWndMain = NULL; WNDCLASSEXW wcex; // // Register the window class // wcex.cbSize = sizeof(WNDCLASSEXW); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC)WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = NULL; wcex.hIcon = NULL; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_BACKGROUND+1); wcex.lpszMenuName = NULL; wcex.lpszClassName = WINDOW_CLASS; wcex.hIconSm = NULL; if (0 == ::RegisterClassExW(&wcex)) { DWORD dwResult = ::GetLastError(); // If we get "Class already exists" error ignore it. We might // encounter this when the user tries to print more than once // in the same setup instance and we are unable to clean up fully. if (dwResult != ERROR_CLASS_ALREADY_EXISTS) { ExitOnFailure(hr = HRESULT_FROM_WIN32(dwResult), "failed to register window class"); } } *pfRegisteredClass = TRUE; // Perform application initialization: hWndMain = ::CreateWindowW(WINDOW_CLASS, NULL, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, NULL, NULL); ExitOnNullWithLastError(hWndMain, hr, "failed to create window for printing"); ::ShowWindow(hWndMain, SW_HIDE); if (!::UpdateWindow(hWndMain)) { ExitWithLastError(hr, "failed to update window"); } *phWndMain = hWndMain; LExit: return hr; } /******************************************************************** PrintRichText - Sends messages to load the Eula text, print it, and close the window. NOTE: Returns errors that have occured while attempting to print, which were saved in vhr by the print callbacks. ********************************************************************/ HRESULT PrintRichText( __in HWND hWndMain ) { MSG msg; // Populate the RichEdit control ::SendMessageW(hWndMain, WM_COMMAND, IDM_POPULATE, 0); // Print Eula ::SendMessageW(hWndMain, WM_COMMAND, IDM_PRINT, 0); // Done! Lets close the Window ::SendMessage(hWndMain, WM_CLOSE, 0, 0); // Main message loop: while (::GetMessageW(&msg, NULL, 0, 0)) { // if (!::TranslateAcceleratorW(msg.hwnd, NULL, &msg)) // { // ::TranslateMessage(&msg); // ::DispatchMessageW(&msg); // } } // return any errors encountered in the print callbacks return vhr; } /******************************************************************** WndProc - Windows callback procedure ********************************************************************/ LRESULT CALLBACK WndProc( __in HWND hWnd, __in UINT message, __in WPARAM wParam, __in LPARAM lParam ) { static HWND hWndRichEdit = NULL; int wmId, wmEvent; PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_CREATE: hWndRichEdit = ::CreateWindowExW(WS_EX_CLIENTEDGE, vwzRichEditClass, L"", ES_MULTILINE | WS_CHILD | WS_VISIBLE | WS_VSCROLL, CONTROL_X_COORDINATE, CONTROL_Y_COORDINATE, CONTROL_WIDTH, CONTROL_HEIGHT, hWnd, NULL, NULL, NULL); break; case WM_COMMAND: wmId = LOWORD(wParam); wmEvent = HIWORD(wParam); switch (wmId) { case IDM_POPULATE: LoadEulaText(hWndRichEdit); break; case IDM_PRINT: Print(hWndRichEdit); break; default: return ::DefWindowProcW(hWnd, message, wParam, lParam); break; } break; case WM_PAINT: hdc = ::BeginPaint(hWnd, &ps); ::EndPaint(hWnd, &ps); break; case WM_DESTROY: ::PostQuitMessage(0); break; default: return ::DefWindowProcW(hWnd, message, wParam, lParam); } return 0; } /******************************************************************** ReadStreamCallback - Callback function to read data to the RichText control NOTE: Richtext control uses this function to read data from the buffer ********************************************************************/ DWORD CALLBACK ReadStreamCallback( __in DWORD /*Cookie*/, __out LPBYTE pbBuff, __in LONG cb, __out LONG FAR *pcb ) { static LPCSTR pszTextBuf = NULL; DWORD er = ERROR_SUCCESS; // If it's null set it to the beginning of the EULA buffer if (pszTextBuf == NULL) { pszTextBuf = vpszEulaText; } LONG lTextLength = (LONG)lstrlen(pszTextBuf); if (cb < 0) { *pcb = 0; er = 1; } else if (lTextLength < cb ) // If the size to be written is less than then length of the buffer, write the rest { *pcb = lTextLength; memcpy(pbBuff, pszTextBuf, *pcb); pszTextBuf = NULL; } else // Only write the amount being asked for and move the pointer along { *pcb = cb; memcpy(pbBuff, pszTextBuf, *pcb); pszTextBuf = pszTextBuf + cb; } return er; } /******************************************************************** LoadEulaText - Reads data for Richedit control ********************************************************************/ void LoadEulaText( __in HWND hWnd ) { HRESULT hr = S_OK; ExitOnNull(hWnd, hr, ERROR_INVALID_HANDLE, "Invalid Handle passed to LoadEulaText"); // Docs say this doesn't return any value ::SendMessageW(hWnd, EM_LIMITTEXT, static_cast<WPARAM>(lstrlen(vpszEulaText)), 0); EDITSTREAM es; ::ZeroMemory(&es, sizeof(es)); es.pfnCallback = (EDITSTREAMCALLBACK)ReadStreamCallback; es.dwCookie = (DWORD)0; ::SendMessageW(hWnd, EM_STREAMIN, SF_RTF, (LPARAM)&es); if (0 != es.dwError) { ExitOnLastError(hr, "failed to load the EULA into the control"); } LExit: vhr = hr; } /******************************************************************** ReadEulaText - Reads Eula text from the MSI ********************************************************************/ HRESULT ReadEulaText( __in MSIHANDLE /*hInstall*/, __out LPSTR* ppszEulaText ) { HRESULT hr = S_OK; PMSIHANDLE hDB; PMSIHANDLE hView; PMSIHANDLE hRec; LPWSTR pwzEula = NULL; hr = WcaOpenExecuteView(vcsEulaQuery, &hView); ExitOnFailure(hr, "failed to open and execute view for PrintEula query"); hr = WcaFetchSingleRecord(hView, &hRec); ExitOnFailure(hr, "failed to fetch the row containing the LicenseText"); hr = WcaGetRecordString(hRec, 1, &pwzEula); ExitOnFailure(hr, "failed to get LicenseText in PrintEula"); hr = StrAnsiAllocString(ppszEulaText, pwzEula, 0, CP_ACP); ExitOnFailure(hr, "failed to convert LicenseText to ANSI code page"); LExit: return hr; } /******************************************************************** Print - Function that sends the data from richedit control to the printer NOTE: Any errors encountered are saved to the vhr variable ********************************************************************/ void Print( __in_opt HWND hRtfWnd ) { HRESULT hr = S_OK; FORMATRANGE fRange; RECT rcPage; RECT rcPrintablePage; GETTEXTLENGTHEX gTxex; HDC hPrinterDC = vpPrintDlg->hDC; int nHorizRes = ::GetDeviceCaps(hPrinterDC, HORZRES); int nVertRes = ::GetDeviceCaps(hPrinterDC, VERTRES); int nLogPixelsX = ::GetDeviceCaps(hPrinterDC, LOGPIXELSX); //int nLogPixelsY = ::GetDeviceCaps(hPrinterDC, LOGPIXELSY); LONG_PTR lTextLength = 0; // Length of document. LONG_PTR lTextPrinted = 0; // Amount of document printed. DOCINFOW dInfo; LPDEVNAMES pDevnames; LPWSTR sczProductName = NULL; BOOL fStartedDoc = FALSE; BOOL fPrintedSomething = FALSE; // Ensure the printer DC is in MM_TEXT mode. if (0 == ::SetMapMode(hPrinterDC, MM_TEXT)) { ExitWithLastError(hr, "failed to set map mode"); } // Rendering to the same DC we are measuring. ::ZeroMemory(&fRange, sizeof(fRange)); fRange.hdc = fRange.hdcTarget = hPrinterDC; // Set up the page. rcPage.left = rcPage.top = 0; rcPage.right = MulDiv(nHorizRes, ONE_INCH, nLogPixelsX); rcPage.bottom = MulDiv(nVertRes, ONE_INCH, nLogPixelsX); // Set up 1" margins all around. rcPrintablePage.left = rcPage.left + ONE_INCH; rcPrintablePage.top = rcPage.top + ONE_INCH; rcPrintablePage.right = rcPage.right - ONE_INCH; rcPrintablePage.bottom = rcPage.bottom - ONE_INCH; // Set up the print job (standard printing stuff here). ::ZeroMemory(&dInfo, sizeof(dInfo)); dInfo.cbSize = sizeof(DOCINFO); hr = WcaGetProperty(L"ProductName", &sczProductName); if (FAILED(hr)) { // If we fail to get the product name, don't fail, just leave it blank; dInfo.lpszDocName = L""; hr = S_OK; } else { dInfo.lpszDocName = sczProductName; } pDevnames = (LPDEVNAMES)::GlobalLock(vpPrintDlg->hDevNames); ExitOnNullWithLastError(pDevnames, hr, "failed to get global lock"); dInfo.lpszOutput = (LPWSTR)pDevnames + pDevnames->wOutputOffset; if (0 == ::GlobalUnlock(pDevnames)) { ExitWithLastError(hr, "failed to release global lock"); } // Start the document. if (0 >= ::StartDocW(hPrinterDC, &dInfo)) { ExitWithLastError(hr, "failed to start print document"); } fStartedDoc = TRUE; ::ZeroMemory(&gTxex, sizeof(gTxex)); gTxex.flags = GTL_NUMCHARS | GTL_PRECISE; lTextLength = ::SendMessageW(hRtfWnd, EM_GETTEXTLENGTHEX, (LONG_PTR)&gTxex, 0); while (lTextPrinted < lTextLength) { // Start the page. if (0 >= ::StartPage(hPrinterDC)) { ExitWithLastError(hr, "failed to start print page"); } // Always reset to the full printable page and start where the // last text left off (or zero at the beginning). fRange.rc = rcPrintablePage; fRange.rcPage = rcPage; fRange.chrg.cpMin = (LONG)lTextPrinted; fRange.chrg.cpMax = -1; // Print as much text as can fit on a page. The return value is // the index of the first character on the next page. Using TRUE // for the wParam parameter causes the text to be printed. lTextPrinted = ::SendMessageW(hRtfWnd, EM_FORMATRANGE, TRUE, (LPARAM)&fRange); fPrintedSomething = TRUE; // If text wasn't printed (i.e. we didn't move past the point we started) then // something must have gone wrong. if (lTextPrinted <= fRange.chrg.cpMin) { hr = E_FAIL; ExitOnFailure(hr, "failed to print some text"); } // Print last page. if (0 >= ::EndPage(hPrinterDC)) { ExitWithLastError(hr, "failed to end print page"); } } LExit: // Tell the control to release cached information, if we actually tried to // print something. if (fPrintedSomething) { ::SendMessageW(hRtfWnd, EM_FORMATRANGE, 0, (LPARAM)NULL); } if (fStartedDoc) { ::EndDoc(hPrinterDC); } ReleaseStr(sczProductName); vhr = hr; } /******************************************************************** ShowErrorMessage - Display MessageBox showing the message for hr. ********************************************************************/ void ShowErrorMessage( __in HRESULT hr ) { WCHAR wzMsg[STRING_CAPACITY]; #pragma prefast(push) #pragma prefast(disable:25028) if (0 != ::FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, 0, hr, 0, wzMsg, countof(wzMsg), 0)) #pragma prefast(pop) { HWND hWnd = ::GetForegroundWindow(); ::MessageBoxW(hWnd, wzMsg, L"PrintEULA", MB_OK | MB_ICONWARNING); } }