root/cloud_print/virtual_driver/win/port_monitor/port_monitor.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. GetAppDataDir
  2. DeleteLeakedFiles
  3. GetJobTitle
  4. HandlePortUi
  5. GetUserToken
  6. LaunchPrintDialog
  7. LaunchChromeDownloadPage
  8. ValidateCurrentUser
  9. ReadPathFromRegistry
  10. ReadPathFromAnyRegistry
  11. GetChromeExePath
  12. GetChromeProfilePath
  13. Monitor2EnumPorts
  14. Monitor2OpenPort
  15. Monitor2StartDocPort
  16. Monitor2WritePort
  17. Monitor2ReadPort
  18. Monitor2EndDocPort
  19. Monitor2ClosePort
  20. Monitor2Shutdown
  21. Monitor2XcvOpenPort
  22. Monitor2XcvDataPort
  23. Monitor2XcvClosePort
  24. MonitorUiAddPortUi
  25. MonitorUiConfigureOrDeletePortUI
  26. InitializePrintMonitor2
  27. InitializePrintMonitorUI

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "cloud_print/virtual_driver/win/port_monitor/port_monitor.h"

#include <lmcons.h>
#include <shellapi.h>
#include <shlobj.h>
#include <strsafe.h>
#include <userenv.h>
#include <windows.h>
#include <winspool.h>

#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/file_util.h"
#include "base/files/file_enumerator.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/process/process.h"
#include "base/process/launch.h"
#include "base/strings/string16.h"
#include "base/win/registry.h"
#include "base/win/scoped_handle.h"
#include "base/win/windows_version.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/installer/launcher_support/chrome_launcher_support.h"
#include "cloud_print/common/win/cloud_print_utils.h"
#include "cloud_print/virtual_driver/win/port_monitor/spooler_win.h"
#include "cloud_print/virtual_driver/win/virtual_driver_consts.h"
#include "cloud_print/virtual_driver/win/virtual_driver_helpers.h"

namespace cloud_print {

namespace {

const wchar_t kIePath[] = L"Internet Explorer\\iexplore.exe";

const char kChromeInstallUrl[] =
    "http://google.com/cloudprint/learn/chrome.html";

const wchar_t kCloudPrintRegKey[] = L"Software\\Google\\CloudPrint";

const wchar_t kXpsMimeType[] = L"application/vnd.ms-xpsdocument";

const wchar_t kAppDataDir[] = L"Google\\Cloud Printer";

struct MonitorData {
  scoped_ptr<base::AtExitManager> at_exit_manager;
};

struct PortData {
  PortData() : job_id(0), printer_handle(NULL), file(0) {
  }
  ~PortData() {
    Close();
  }
  void Close() {
    if (printer_handle) {
      ClosePrinter(printer_handle);
      printer_handle = NULL;
    }
    if (file) {
      base::CloseFile(file);
      file = NULL;
    }
  }
  DWORD job_id;
  HANDLE printer_handle;
  FILE* file;
  base::FilePath file_path;
};

typedef struct {
  ACCESS_MASK granted_access;
} XcvUiData;


MONITORUI g_monitor_ui = {
  sizeof(MONITORUI),
  MonitorUiAddPortUi,
  MonitorUiConfigureOrDeletePortUI,
  MonitorUiConfigureOrDeletePortUI
};

MONITOR2 g_monitor_2 = {
  sizeof(MONITOR2),
  Monitor2EnumPorts,
  Monitor2OpenPort,
  NULL,           // OpenPortEx is not supported.
  Monitor2StartDocPort,
  Monitor2WritePort,
  Monitor2ReadPort,
  Monitor2EndDocPort,
  Monitor2ClosePort,
  NULL,           // AddPort is not supported.
  NULL,           // AddPortEx is not supported.
  NULL,           // ConfigurePort is not supported.
  NULL,           // DeletePort is not supported.
  NULL,
  NULL,           // SetPortTimeOuts is not supported.
  Monitor2XcvOpenPort,
  Monitor2XcvDataPort,
  Monitor2XcvClosePort,
  Monitor2Shutdown
};

base::FilePath GetAppDataDir() {
  base::FilePath file_path;
  base::win::Version version = base::win::GetVersion();
  int path_id = (version >= base::win::VERSION_VISTA) ?
                base::DIR_LOCAL_APP_DATA_LOW : base::DIR_LOCAL_APP_DATA;
  if (!PathService::Get(path_id, &file_path)) {
    LOG(ERROR) << "Can't get DIR_LOCAL_APP_DATA";
    return base::FilePath();
  }
  return file_path.Append(kAppDataDir);
}

// Delete files which where not deleted by chrome.
void DeleteLeakedFiles(const base::FilePath& dir) {
  base::Time delete_before = base::Time::Now() - base::TimeDelta::FromDays(1);
  base::FileEnumerator enumerator(dir, false, base::FileEnumerator::FILES);
  for (base::FilePath file_path = enumerator.Next(); !file_path.empty();
       file_path = enumerator.Next()) {
    if (enumerator.GetInfo().GetLastModifiedTime() < delete_before)
      base::DeleteFile(file_path, false);
  }
}

// Attempts to retrieve the title of the specified print job.
// On success returns TRUE and the first title_chars characters of the job title
// are copied into title.
// On failure returns FALSE and title is unmodified.
bool GetJobTitle(HANDLE printer_handle,
                 DWORD job_id,
                 base::string16 *title) {
  DCHECK(printer_handle != NULL);
  DCHECK(title != NULL);
  DWORD bytes_needed = 0;
  GetJob(printer_handle, job_id, 1, NULL, 0, &bytes_needed);
  if (bytes_needed == 0) {
    LOG(ERROR) << "Unable to get bytes needed for job info.";
    return false;
  }
  scoped_ptr<BYTE[]> buffer(new BYTE[bytes_needed]);
  if (!GetJob(printer_handle,
              job_id,
              1,
              buffer.get(),
              bytes_needed,
              &bytes_needed)) {
    LOG(ERROR) << "Unable to get job info.";
    return false;
  }
  JOB_INFO_1* job_info = reinterpret_cast<JOB_INFO_1*>(buffer.get());
  *title = job_info->pDocument;
  return true;
}

// Handler for the UI functions exported by the port monitor.
// Verifies that a valid parent Window exists and then just displays an
// error message to let the user know that there is no interactive
// configuration.
void HandlePortUi(HWND hwnd, const base::string16& caption) {
  if (hwnd != NULL && IsWindow(hwnd)) {
    DisplayWindowsMessage(hwnd, CO_E_NOT_SUPPORTED, cloud_print::kPortName);
  }
}

// Gets the primary token for the user that submitted the print job.
bool GetUserToken(HANDLE* primary_token) {
  HANDLE token = NULL;
  if (!OpenThreadToken(GetCurrentThread(),
                      TOKEN_QUERY|TOKEN_DUPLICATE|TOKEN_ASSIGN_PRIMARY,
                      FALSE,
                      &token)) {
    LOG(ERROR) << "Unable to get thread token.";
    return false;
  }
  base::win::ScopedHandle token_scoped(token);
  if (!DuplicateTokenEx(token,
                        TOKEN_QUERY|TOKEN_DUPLICATE|TOKEN_ASSIGN_PRIMARY,
                        NULL,
                        SecurityImpersonation,
                        TokenPrimary,
                        primary_token)) {
    LOG(ERROR) << "Unable to get primary thread token.";
    return false;
  }
  return true;
}

// Launches the Cloud Print dialog in Chrome.
// xps_path references a file to print.
// job_title is the title to be used for the resulting print job.
bool LaunchPrintDialog(const base::FilePath& xps_path,
                       const base::string16& job_title) {
  HANDLE token = NULL;
  if (!GetUserToken(&token)) {
    LOG(ERROR) << "Unable to get user token.";
    return false;
  }
  base::win::ScopedHandle primary_token_scoped(token);

  base::FilePath chrome_path = GetChromeExePath();
  if (chrome_path.empty()) {
    LOG(ERROR) << "Unable to get chrome exe path.";
    return false;
  }

  CommandLine command_line(chrome_path);

  base::FilePath chrome_profile = GetChromeProfilePath();
  if (!chrome_profile.empty())
    command_line.AppendSwitchPath(switches::kUserDataDir, chrome_profile);

  command_line.AppendSwitchPath(switches::kCloudPrintFile, xps_path);
  command_line.AppendSwitchNative(switches::kCloudPrintFileType, kXpsMimeType);
  command_line.AppendSwitchNative(switches::kCloudPrintJobTitle, job_title);
  base::LaunchOptions options;
  options.as_user = primary_token_scoped;
  base::LaunchProcess(command_line, options, NULL);
  return true;
}

// Launches a page to allow the user to download chrome.
// TODO(abodenha@chromium.org) Point to a custom page explaining what's wrong
// rather than the generic chrome download page.  See
// http://code.google.com/p/chromium/issues/detail?id=112019
void LaunchChromeDownloadPage() {
  if (kIsUnittest)
    return;
  HANDLE token = NULL;
  if (!GetUserToken(&token)) {
    LOG(ERROR) << "Unable to get user token.";
    return;
  }
  base::win::ScopedHandle token_scoped(token);

  base::FilePath ie_path;
  PathService::Get(base::DIR_PROGRAM_FILESX86, &ie_path);
  ie_path = ie_path.Append(kIePath);
  CommandLine command_line(ie_path);
  command_line.AppendArg(kChromeInstallUrl);

  base::LaunchOptions options;
  options.as_user = token_scoped;
  base::LaunchProcess(command_line, options, NULL);
}

// Returns false if the print job is being run in a context
// that shouldn't be launching Chrome.
bool ValidateCurrentUser() {
  HANDLE token = NULL;
  if (!GetUserToken(&token)) {
    // If we can't get the token we're probably not impersonating
    // the user, so validation should fail.
    return false;
  }
  base::win::ScopedHandle token_scoped(token);

  if (base::win::GetVersion() >= base::win::VERSION_VISTA) {
    DWORD session_id = 0;
    DWORD dummy;
    if (!GetTokenInformation(token_scoped,
                             TokenSessionId,
                             reinterpret_cast<void *>(&session_id),
                             sizeof(DWORD),
                             &dummy)) {
      return false;
    }
    if (session_id == 0) {
      return false;
    }
  }
  return true;
}
}  // namespace

base::FilePath ReadPathFromRegistry(HKEY root, const wchar_t* path_name) {
  base::win::RegKey gcp_key(HKEY_CURRENT_USER, kCloudPrintRegKey, KEY_READ);
  base::string16 data;
  if (SUCCEEDED(gcp_key.ReadValue(path_name, &data)) &&
      base::PathExists(base::FilePath(data))) {
    return base::FilePath(data);
  }
  return base::FilePath();
}

base::FilePath ReadPathFromAnyRegistry(const wchar_t* path_name) {
  base::FilePath result = ReadPathFromRegistry(HKEY_CURRENT_USER, path_name);
  if (!result.empty())
    return result;
  return ReadPathFromRegistry(HKEY_LOCAL_MACHINE, path_name);
}

base::FilePath GetChromeExePath() {
  base::FilePath path = ReadPathFromAnyRegistry(kChromeExePathRegValue);
  if (!path.empty())
    return path;
  return chrome_launcher_support::GetAnyChromePath();
}

base::FilePath GetChromeProfilePath() {
  base::FilePath path = ReadPathFromAnyRegistry(kChromeProfilePathRegValue);
  if (!path.empty() && base::DirectoryExists(path))
    return path;
  return base::FilePath();
}

BOOL WINAPI Monitor2EnumPorts(HANDLE,
                              wchar_t*,
                              DWORD level,
                              BYTE*  ports,
                              DWORD   ports_size,
                              DWORD* needed_bytes,
                              DWORD* returned) {
  if (needed_bytes == NULL) {
    LOG(ERROR) << "needed_bytes should not be NULL.";
    SetLastError(ERROR_INVALID_PARAMETER);
    return FALSE;
  }
  if (level == 1) {
    *needed_bytes = sizeof(PORT_INFO_1);
  } else if (level == 2) {
    *needed_bytes = sizeof(PORT_INFO_2);
  } else {
    LOG(ERROR) << "Level "  << level << "is not supported.";
    SetLastError(ERROR_INVALID_LEVEL);
    return FALSE;
  }
  *needed_bytes += static_cast<DWORD>(cloud_print::kPortNameSize);
  if (ports_size < *needed_bytes) {
    LOG(WARNING) << *needed_bytes << " bytes are required.  Only "
                 << ports_size << " were allocated.";
    SetLastError(ERROR_INSUFFICIENT_BUFFER);
    return FALSE;
  }
  if (ports == NULL) {
    LOG(ERROR) << "ports should not be NULL.";
    SetLastError(ERROR_INVALID_PARAMETER);
    return FALSE;
  }
  if (returned == NULL) {
    LOG(ERROR) << "returned should not be NULL.";
    SetLastError(ERROR_INVALID_PARAMETER);
    return FALSE;
  }

  // Windows expects any strings refernced by PORT_INFO_X structures to
  // appear at the END of the buffer referenced by ports.  Placing
  // strings immediately after the PORT_INFO_X structure will cause
  // EnumPorts to fail until the spooler is restarted.
  // This is NOT mentioned in the documentation.
  wchar_t* string_target =
      reinterpret_cast<wchar_t*>(ports + ports_size -
      cloud_print::kPortNameSize);
  if (level == 1) {
    PORT_INFO_1* port_info = reinterpret_cast<PORT_INFO_1*>(ports);
    port_info->pName = string_target;
    StringCbCopy(port_info->pName,
                 cloud_print::kPortNameSize,
                 cloud_print::kPortName);
  } else {
    PORT_INFO_2* port_info = reinterpret_cast<PORT_INFO_2*>(ports);
    port_info->pPortName = string_target;
    StringCbCopy(port_info->pPortName,
                 cloud_print::kPortNameSize,
                 cloud_print::kPortName);
    port_info->pMonitorName = NULL;
    port_info->pDescription = NULL;
    port_info->fPortType = PORT_TYPE_WRITE;
    port_info->Reserved = 0;
  }
  *returned = 1;
  return TRUE;
}

BOOL WINAPI Monitor2OpenPort(HANDLE, wchar_t*, HANDLE* handle) {
  if (handle == NULL) {
    LOG(ERROR) << "handle should not be NULL.";
    SetLastError(ERROR_INVALID_PARAMETER);
    return FALSE;
  }
  *handle = new PortData();
  return TRUE;
}

BOOL WINAPI Monitor2StartDocPort(HANDLE port_handle,
                                 wchar_t* printer_name,
                                 DWORD job_id,
                                 DWORD,
                                 BYTE*) {
  SetGoogleUpdateUsage(kGoogleUpdateProductId);
  if (port_handle == NULL) {
    LOG(ERROR) << "port_handle should not be NULL.";
    SetLastError(ERROR_INVALID_PARAMETER);
    return FALSE;
  }
  if (printer_name == NULL) {
    LOG(ERROR) << "printer_name should not be NULL.";
    SetLastError(ERROR_INVALID_PARAMETER);
    return FALSE;
  }
  if (!ValidateCurrentUser()) {
    // TODO(abodenha@chromium.org) Abort the print job.
    return FALSE;
  }
  PortData* port_data = reinterpret_cast<PortData*>(port_handle);
  port_data->job_id = job_id;
  if (!OpenPrinter(printer_name, &(port_data->printer_handle), NULL)) {
    LOG(WARNING) << "Unable to open printer " << printer_name << ".";
    // We can continue without a handle to the printer.
    // It just means we can't get the job title or tell the spooler that
    // the print job is complete.
    // This is the normal flow during a unit test.
    port_data->printer_handle = NULL;
  }
  base::FilePath& file_path = port_data->file_path;
  base::FilePath app_data_dir = GetAppDataDir();
  if (app_data_dir.empty())
    return FALSE;
  DeleteLeakedFiles(app_data_dir);
  if (!base::CreateDirectory(app_data_dir) ||
      !base::CreateTemporaryFileInDir(app_data_dir, &file_path)) {
    LOG(ERROR) << "Can't create temporary file in " << app_data_dir.value();
    return FALSE;
  }
  port_data->file = base::OpenFile(file_path, "wb+");
  if (port_data->file == NULL) {
    LOG(ERROR) << "Error opening file " << file_path.value() << ".";
    return FALSE;
  }
  return TRUE;
}

BOOL WINAPI Monitor2WritePort(HANDLE port_handle,
                              BYTE* buffer,
                              DWORD buffer_size,
                              DWORD* bytes_written) {
  PortData* port_data = reinterpret_cast<PortData*>(port_handle);
  if (!ValidateCurrentUser()) {
    // TODO(abodenha@chromium.org) Abort the print job.
    return FALSE;
  }
  *bytes_written =
      static_cast<DWORD>(fwrite(buffer, 1, buffer_size, port_data->file));
  if (*bytes_written > 0) {
    return TRUE;
  } else {
    return FALSE;
  }
}

BOOL WINAPI Monitor2ReadPort(HANDLE, BYTE*, DWORD, DWORD* read_bytes) {
  LOG(ERROR) << "Read is not supported.";
  *read_bytes = 0;
  SetLastError(ERROR_NOT_SUPPORTED);
  return FALSE;
}

BOOL WINAPI Monitor2EndDocPort(HANDLE port_handle) {
  if (!ValidateCurrentUser()) {
    // TODO(abodenha@chromium.org) Abort the print job.
    return FALSE;
  }
  PortData* port_data = reinterpret_cast<PortData*>(port_handle);
  if (port_data == NULL) {
    SetLastError(ERROR_INVALID_PARAMETER);
    return FALSE;
  }

  if (port_data->file != NULL) {
    base::CloseFile(port_data->file);
    port_data->file = NULL;
    bool delete_file = true;
    int64 file_size = 0;
    base::GetFileSize(port_data->file_path, &file_size);
    if (file_size > 0) {
      base::string16 job_title;
      if (port_data->printer_handle != NULL) {
        GetJobTitle(port_data->printer_handle,
                    port_data->job_id,
                    &job_title);
      }
      if (!LaunchPrintDialog(port_data->file_path, job_title)) {
        LaunchChromeDownloadPage();
      } else {
        delete_file = false;
      }
    }
    if (delete_file)
      base::DeleteFile(port_data->file_path, false);
  }
  if (port_data->printer_handle != NULL) {
    // Tell the spooler that the job is complete.
    SetJob(port_data->printer_handle,
           port_data->job_id,
           0,
           NULL,
           JOB_CONTROL_SENT_TO_PRINTER);
  }
  port_data->Close();
  // Return success even if we can't display the dialog.
  // TODO(abodenha@chromium.org) Come up with a better way of handling
  // this situation.
  return TRUE;
}

BOOL WINAPI Monitor2ClosePort(HANDLE port_handle) {
  if (port_handle == NULL) {
    LOG(ERROR) << "port_handle should not be NULL.";
    SetLastError(ERROR_INVALID_PARAMETER);
    return FALSE;
  }
  PortData* port_data = reinterpret_cast<PortData*>(port_handle);
  delete port_data;
  return TRUE;
}

VOID WINAPI Monitor2Shutdown(HANDLE monitor_handle) {
  if (monitor_handle != NULL) {
    MonitorData* monitor_data =
      reinterpret_cast<MonitorData*>(monitor_handle);
    delete monitor_handle;
  }
}

BOOL WINAPI Monitor2XcvOpenPort(HANDLE,
                                const wchar_t*,
                                ACCESS_MASK granted_access,
                                HANDLE* handle) {
  if (handle == NULL) {
    LOG(ERROR) << "handle should not be NULL.";
    SetLastError(ERROR_INVALID_PARAMETER);
    return FALSE;
  }
  XcvUiData* xcv_data = new XcvUiData();
  xcv_data->granted_access = granted_access;
  *handle = xcv_data;
  return TRUE;
}

DWORD WINAPI Monitor2XcvDataPort(HANDLE xcv_handle,
                                 const wchar_t* data_name,
                                 BYTE*,
                                 DWORD,
                                 BYTE* output_data,
                                 DWORD output_data_bytes,
                                 DWORD* output_data_bytes_needed) {
  XcvUiData* xcv_data = reinterpret_cast<XcvUiData*>(xcv_handle);
  DWORD ret_val = ERROR_SUCCESS;
  if ((xcv_data->granted_access & SERVER_ACCESS_ADMINISTER) == 0) {
    return ERROR_ACCESS_DENIED;
  }
  if (output_data == NULL || output_data_bytes == 0) {
    return ERROR_INVALID_PARAMETER;
  }
  // We don't handle AddPort or DeletePort since we don't support
  // dynamic creation of ports.
  if (lstrcmp(L"MonitorUI", data_name) == 0) {
    DWORD dll_path_len = 0;
    base::FilePath dll_path(GetPortMonitorDllName());
    dll_path_len = static_cast<DWORD>(dll_path.value().length());
    if (output_data_bytes_needed != NULL) {
      *output_data_bytes_needed = dll_path_len;
    }
    if (output_data_bytes < dll_path_len) {
        return ERROR_INSUFFICIENT_BUFFER;
    } else {
        ret_val = StringCbCopy(reinterpret_cast<wchar_t*>(output_data),
                               output_data_bytes,
                               dll_path.value().c_str());
    }
  } else {
    return ERROR_INVALID_PARAMETER;
  }
  return ret_val;
}

BOOL WINAPI Monitor2XcvClosePort(HANDLE handle) {
  XcvUiData* xcv_data = reinterpret_cast<XcvUiData*>(handle);
  delete xcv_data;
  return TRUE;
}

BOOL WINAPI MonitorUiAddPortUi(const wchar_t*,
                               HWND hwnd,
                               const wchar_t* monitor_name,
                               wchar_t**) {
  HandlePortUi(hwnd, monitor_name);
  return TRUE;
}

BOOL WINAPI MonitorUiConfigureOrDeletePortUI(const wchar_t*,
                                             HWND hwnd,
                                             const wchar_t* port_name) {
  HandlePortUi(hwnd, port_name);
  return TRUE;
}

}   // namespace cloud_print

MONITOR2* WINAPI InitializePrintMonitor2(MONITORINIT*,
                                         HANDLE* handle) {
  if (handle == NULL) {
    SetLastError(ERROR_INVALID_PARAMETER);
    return NULL;
  }
  cloud_print::MonitorData* monitor_data = new cloud_print::MonitorData;
  *handle = monitor_data;
  if (!cloud_print::kIsUnittest) {
    // Unit tests set up their own AtExitManager
    monitor_data->at_exit_manager.reset(new base::AtExitManager());
    // Single spooler.exe handles verbose users.
    PathService::DisableCache();
  }
  return &cloud_print::g_monitor_2;
}

MONITORUI* WINAPI InitializePrintMonitorUI(void) {
  return &cloud_print::g_monitor_ui;
}


/* [<][>][^][v][top][bottom][index][help] */