// Copyright (c) 2011 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 "chrome/browser/plugin_updater.h"

#include <string>

#include "base/memory/scoped_ptr.h"
#include "base/message_loop.h"
#include "base/path_service.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "base/version.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/prefs/scoped_user_pref_update.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_content_client.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "content/browser/browser_thread.h"
#include "content/common/notification_service.h"
#include "webkit/plugins/npapi/plugin_list.h"
#include "webkit/plugins/npapi/webplugininfo.h"

// How long to wait to save the plugin enabled information, which might need to
// go to disk.
#define kPluginUpdateDelayMs (60 * 1000)

PluginUpdater::PluginUpdater()
    : notify_pending_(false) {
}

DictionaryValue* PluginUpdater::CreatePluginFileSummary(
    const webkit::npapi::WebPluginInfo& plugin) {
  DictionaryValue* data = new DictionaryValue();
  data->SetString("path", plugin.path.value());
  data->SetString("name", plugin.name);
  data->SetString("version", plugin.version);
  data->SetBoolean("enabled", webkit::npapi::IsPluginEnabled(plugin));
  return data;
}

// static
ListValue* PluginUpdater::GetPluginGroupsData() {
  std::vector<webkit::npapi::PluginGroup> plugin_groups;
  webkit::npapi::PluginList::Singleton()->GetPluginGroups(true, &plugin_groups);

  // Construct DictionaryValues to return to the UI
  ListValue* plugin_groups_data = new ListValue();
  for (size_t i = 0; i < plugin_groups.size(); ++i) {
    plugin_groups_data->Append(plugin_groups[i].GetDataForUI());
  }
  return plugin_groups_data;
}

void PluginUpdater::EnablePluginGroup(bool enable, const string16& group_name) {
  webkit::npapi::PluginList::Singleton()->EnableGroup(enable, group_name);
  NotifyPluginStatusChanged();
}

void PluginUpdater::EnablePlugin(bool enable,
                                 const FilePath::StringType& path) {
  FilePath file_path(path);
  if (enable)
    webkit::npapi::PluginList::Singleton()->EnablePlugin(file_path);
  else
    webkit::npapi::PluginList::Singleton()->DisablePlugin(file_path);

  NotifyPluginStatusChanged();
}

void PluginUpdater::Observe(NotificationType type,
                            const NotificationSource& source,
                            const NotificationDetails& details) {
  DCHECK_EQ(NotificationType::PREF_CHANGED, type.value);
  const std::string* pref_name = Details<std::string>(details).ptr();
  if (!pref_name) {
    NOTREACHED();
    return;
  }
  if (*pref_name == prefs::kPluginsDisabledPlugins ||
      *pref_name == prefs::kPluginsDisabledPluginsExceptions ||
      *pref_name == prefs::kPluginsEnabledPlugins) {
    PrefService* pref_service = Source<PrefService>(source).ptr();
    const ListValue* disabled_list =
        pref_service->GetList(prefs::kPluginsDisabledPlugins);
    const ListValue* exceptions_list =
        pref_service->GetList(prefs::kPluginsDisabledPluginsExceptions);
    const ListValue* enabled_list =
        pref_service->GetList(prefs::kPluginsEnabledPlugins);
    UpdatePluginsStateFromPolicy(disabled_list, exceptions_list, enabled_list);
  }
}

void PluginUpdater::UpdatePluginsStateFromPolicy(
    const ListValue* disabled_list,
    const ListValue* exceptions_list,
    const ListValue* enabled_list) {
  std::set<string16> disabled_plugin_patterns;
  std::set<string16> disabled_plugin_exception_patterns;
  std::set<string16> enabled_plugin_patterns;

  ListValueToStringSet(disabled_list, &disabled_plugin_patterns);
  ListValueToStringSet(exceptions_list, &disabled_plugin_exception_patterns);
  ListValueToStringSet(enabled_list, &enabled_plugin_patterns);

  webkit::npapi::PluginGroup::SetPolicyEnforcedPluginPatterns(
      disabled_plugin_patterns,
      disabled_plugin_exception_patterns,
      enabled_plugin_patterns);

  NotifyPluginStatusChanged();
}

void PluginUpdater::ListValueToStringSet(const ListValue* src,
                                         std::set<string16>* dest) {
  DCHECK(src);
  DCHECK(dest);
  ListValue::const_iterator end(src->end());
  for (ListValue::const_iterator current(src->begin());
       current != end; ++current) {
    string16 plugin_name;
    if ((*current)->GetAsString(&plugin_name)) {
      dest->insert(plugin_name);
    }
  }
}

void PluginUpdater::UpdatePluginGroupsStateFromPrefs(Profile* profile) {
  bool update_internal_dir = false;
  FilePath last_internal_dir =
  profile->GetPrefs()->GetFilePath(prefs::kPluginsLastInternalDirectory);
  FilePath cur_internal_dir;
  if (PathService::Get(chrome::DIR_INTERNAL_PLUGINS, &cur_internal_dir) &&
      cur_internal_dir != last_internal_dir) {
    update_internal_dir = true;
    profile->GetPrefs()->SetFilePath(
        prefs::kPluginsLastInternalDirectory, cur_internal_dir);
  }

  bool force_enable_internal_pdf = false;
  bool internal_pdf_enabled = false;
  string16 pdf_group_name =
      ASCIIToUTF16(chrome::ChromeContentClient::kPDFPluginName);
  FilePath pdf_path;
  PathService::Get(chrome::FILE_PDF_PLUGIN, &pdf_path);
  FilePath::StringType pdf_path_str = pdf_path.value();
  if (!profile->GetPrefs()->GetBoolean(prefs::kPluginsEnabledInternalPDF)) {
    // We switched to the internal pdf plugin being on by default, and so we
    // need to force it to be enabled.  We only want to do it this once though,
    // i.e. we don't want to enable it again if the user disables it afterwards.
    profile->GetPrefs()->SetBoolean(prefs::kPluginsEnabledInternalPDF, true);
    force_enable_internal_pdf = true;
  }

  {  // Scoped update of prefs::kPluginsPluginsList.
    ListPrefUpdate update(profile->GetPrefs(), prefs::kPluginsPluginsList);
    ListValue* saved_plugins_list = update.Get();
    if (saved_plugins_list) {
      for (ListValue::const_iterator it = saved_plugins_list->begin();
           it != saved_plugins_list->end();
           ++it) {
        if (!(*it)->IsType(Value::TYPE_DICTIONARY)) {
          LOG(WARNING) << "Invalid entry in " << prefs::kPluginsPluginsList;
          continue;  // Oops, don't know what to do with this item.
        }

        DictionaryValue* plugin = static_cast<DictionaryValue*>(*it);
        string16 group_name;
        bool enabled = true;
        plugin->GetBoolean("enabled", &enabled);

        FilePath::StringType path;
        // The plugin list constains all the plugin files in addition to the
        // plugin groups.
        if (plugin->GetString("path", &path)) {
          // Files have a path attribute, groups don't.
          FilePath plugin_path(path);
          if (update_internal_dir &&
              FilePath::CompareIgnoreCase(plugin_path.DirName().value(),
                  last_internal_dir.value()) == 0) {
            // If the internal plugin directory has changed and if the plugin
            // looks internal, update its path in the prefs.
            plugin_path = cur_internal_dir.Append(plugin_path.BaseName());
            path = plugin_path.value();
            plugin->SetString("path", path);
          }

          if (FilePath::CompareIgnoreCase(path, pdf_path_str) == 0) {
            if (!enabled && force_enable_internal_pdf) {
              enabled = true;
              plugin->SetBoolean("enabled", true);
            }

            internal_pdf_enabled = enabled;
          }

          if (!enabled)
            webkit::npapi::PluginList::Singleton()->DisablePlugin(plugin_path);
        } else if (!enabled && plugin->GetString("name", &group_name)) {
          // Don't disable this group if it's for the pdf plugin and we just
          // forced it on.
          if (force_enable_internal_pdf && pdf_group_name == group_name)
            continue;

          // Otherwise this is a list of groups.
          EnablePluginGroup(false, group_name);
        }
      }
    }
  }  // Scoped update of prefs::kPluginsPluginsList.

  // Build the set of policy enabled/disabled plugin patterns once and cache it.
  // Don't do this in the constructor, there's no profile available there.
  const ListValue* disabled_plugins =
      profile->GetPrefs()->GetList(prefs::kPluginsDisabledPlugins);
  const ListValue* disabled_exception_plugins =
      profile->GetPrefs()->GetList(prefs::kPluginsDisabledPluginsExceptions);
  const ListValue* enabled_plugins =
      profile->GetPrefs()->GetList(prefs::kPluginsEnabledPlugins);
  UpdatePluginsStateFromPolicy(disabled_plugins,
                               disabled_exception_plugins,
                               enabled_plugins);

  if (force_enable_internal_pdf || internal_pdf_enabled) {
    // See http://crbug.com/50105 for background.
    EnablePluginGroup(false, ASCIIToUTF16(
        webkit::npapi::PluginGroup::kAdobeReaderGroupName));
  }

  if (force_enable_internal_pdf) {
    // We want to save this, but doing so requires loading the list of plugins,
    // so do it after a minute as to not impact startup performance.  Note that
    // plugins are loaded after 30s by the metrics service.
    UpdatePreferences(profile, kPluginUpdateDelayMs);
  }
}

void PluginUpdater::UpdatePreferences(Profile* profile, int delay_ms) {
  BrowserThread::PostDelayedTask(
    BrowserThread::FILE,
    FROM_HERE,
    NewRunnableFunction(
        &PluginUpdater::GetPreferencesDataOnFileThread, profile), delay_ms);
}

void PluginUpdater::GetPreferencesDataOnFileThread(void* profile) {
  std::vector<webkit::npapi::WebPluginInfo> plugins;
  webkit::npapi::PluginList::Singleton()->GetPlugins(false, &plugins);

  std::vector<webkit::npapi::PluginGroup> groups;
  webkit::npapi::PluginList::Singleton()->GetPluginGroups(false, &groups);

  BrowserThread::PostTask(
    BrowserThread::UI,
    FROM_HERE,
    NewRunnableFunction(&PluginUpdater::OnUpdatePreferences,
                        static_cast<Profile*>(profile),
                        plugins, groups));
}

void PluginUpdater::OnUpdatePreferences(
    Profile* profile,
    const std::vector<webkit::npapi::WebPluginInfo>& plugins,
    const std::vector<webkit::npapi::PluginGroup>& groups) {
  ListPrefUpdate update(profile->GetPrefs(), prefs::kPluginsPluginsList);
  ListValue* plugins_list = update.Get();
  plugins_list->Clear();

  FilePath internal_dir;
  if (PathService::Get(chrome::DIR_INTERNAL_PLUGINS, &internal_dir))
    profile->GetPrefs()->SetFilePath(prefs::kPluginsLastInternalDirectory,
                                     internal_dir);

  // Add the plugin files.
  for (size_t i = 0; i < plugins.size(); ++i) {
    DictionaryValue* summary = CreatePluginFileSummary(plugins[i]);
    // If the plugin is managed by policy, store the user preferred state
    // instead.
    if (plugins[i].enabled & webkit::npapi::WebPluginInfo::MANAGED_MASK) {
      bool user_enabled =
          (plugins[i].enabled & webkit::npapi::WebPluginInfo::USER_MASK) ==
              webkit::npapi::WebPluginInfo::USER_ENABLED;
      summary->SetBoolean("enabled", user_enabled);
    }
    bool enabled_val;
    summary->GetBoolean("enabled", &enabled_val);
    plugins_list->Append(summary);
  }

  // Add the groups as well.
  for (size_t i = 0; i < groups.size(); ++i) {
      DictionaryValue* summary = groups[i].GetSummary();
      // If the plugin is disabled only by policy don't store this state in the
      // user pref store.
      if (!groups[i].Enabled() &&
          webkit::npapi::PluginGroup::IsPluginNameDisabledByPolicy(
              groups[i].GetGroupName()))
        summary->SetBoolean("enabled", true);
      plugins_list->Append(summary);
  }
}

void PluginUpdater::NotifyPluginStatusChanged() {
  if (notify_pending_)
    return;
  notify_pending_ = true;
  MessageLoop::current()->PostTask(
      FROM_HERE,
      NewRunnableFunction(&PluginUpdater::OnNotifyPluginStatusChanged));
}

void PluginUpdater::OnNotifyPluginStatusChanged() {
  GetInstance()->notify_pending_ = false;
  NotificationService::current()->Notify(
      NotificationType::PLUGIN_ENABLE_STATUS_CHANGED,
      Source<PluginUpdater>(GetInstance()),
      NotificationService::NoDetails());
}

/*static*/
PluginUpdater* PluginUpdater::GetInstance() {
  return Singleton<PluginUpdater>::get();
}