/*
 * Copyright 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Author: jefftk@google.com (Jeff Kaufman)

#include "ngx_rewrite_options.h"

extern "C" {
  #include <ngx_config.h>
  #include <ngx_core.h>
  #include <ngx_http.h>
}

#include "ngx_pagespeed.h"
#include "ngx_rewrite_driver_factory.h"

#include "net/instaweb/public/version.h"
#include "net/instaweb/rewriter/public/file_load_policy.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/system/system_caches.h"

namespace net_instaweb {

namespace {

const char kStatisticsPath[] = "StatisticsPath";
const char kGlobalStatisticsPath[] = "GlobalStatisticsPath";
const char kConsolePath[] = "ConsolePath";
const char kMessagesPath[] = "MessagesPath";
const char kAdminPath[] = "AdminPath";
const char kGlobalAdminPath[] = "GlobalAdminPath";

// These options are copied from mod_instaweb.cc, where APACHE_CONFIG_OPTIONX
// indicates that they can not be set at the directory/location level. They set
// options in the RewriteDriverFactory, so they're entirely global and do not
// appear in RewriteOptions.  They are not alphabetized on purpose, but rather
// left in the same order as in mod_instaweb.cc in case we end up needing to
// compare.
// TODO(oschaaf): this duplication is a short term solution.
const char* const server_only_options[] = {
  "FetcherTimeoutMs",
  "FetchProxy",
  "ForceCaching",
  "GeneratedFilePrefix",
  "ImgMaxRewritesAtOnce",
  "InheritVHostConfig",
  "InstallCrashHandler",
  "MessageBufferSize",
  "NumRewriteThreads",
  "NumExpensiveRewriteThreads",
  "StaticAssetPrefix",
  "TrackOriginalContentLength",
  "UsePerVHostStatistics",  // TODO(anupama): What to do about "No longer used"
  "BlockingRewriteRefererUrls",
  "CreateSharedMemoryMetadataCache",
  "LoadFromFile",
  "LoadFromFileMatch",
  "LoadFromFileRule",
  "LoadFromFileRuleMatch",
  "UseNativeFetcher",
  "NativeFetcherMaxKeepaliveRequests"
};

// Options that can only be used in the main (http) option scope.
const char* const main_only_options[] = {
  "UseNativeFetcher",
  "NativeFetcherMaxKeepaliveRequests"
};

}  // namespace

RewriteOptions::Properties* NgxRewriteOptions::ngx_properties_ = NULL;

NgxRewriteOptions::NgxRewriteOptions(const StringPiece& description,
                                     ThreadSystem* thread_system)
    : SystemRewriteOptions(description, thread_system) {
  Init();
}

NgxRewriteOptions::NgxRewriteOptions(ThreadSystem* thread_system)
    : SystemRewriteOptions(thread_system) {
  Init();
}

void NgxRewriteOptions::Init() {
  DCHECK(ngx_properties_ != NULL)
      << "Call NgxRewriteOptions::Initialize() before construction";
  clear_inherited_scripts_ = false;
  InitializeOptions(ngx_properties_);
}

void NgxRewriteOptions::AddProperties() {
  // Nginx-specific options.
  add_ngx_option(
      "", &NgxRewriteOptions::statistics_path_, "nsp", kStatisticsPath,
      kServerScope, "Set the statistics path. Ex: /ngx_pagespeed_statistics",
      false);
  add_ngx_option(
      "", &NgxRewriteOptions::global_statistics_path_, "ngsp",
      kGlobalStatisticsPath, kProcessScopeStrict,
      "Set the global statistics path. Ex: /ngx_pagespeed_global_statistics",
      false);
  add_ngx_option(
      "", &NgxRewriteOptions::console_path_, "ncp", kConsolePath, kServerScope,
      "Set the console path. Ex: /pagespeed_console", false);
  add_ngx_option(
      "", &NgxRewriteOptions::messages_path_, "nmp", kMessagesPath,
      kServerScope, "Set the messages path.  Ex: /ngx_pagespeed_message",
      false);
  add_ngx_option(
      "", &NgxRewriteOptions::admin_path_, "nap", kAdminPath,
      kServerScope, "Set the admin path.  Ex: /pagespeed_admin", false);
  add_ngx_option(
      "", &NgxRewriteOptions::global_admin_path_, "ngap", kGlobalAdminPath,
      kProcessScopeStrict,
      "Set the global admin path.  Ex: /pagespeed_global_admin",
      false);

  MergeSubclassProperties(ngx_properties_);

  // Default properties are global but to set them the current API requires
  // a RewriteOptions instance and we're in a static method.
  NgxRewriteOptions dummy_config(NULL);
  dummy_config.set_default_x_header_value(kModPagespeedVersion);
}

void NgxRewriteOptions::Initialize() {
  if (Properties::Initialize(&ngx_properties_)) {
    SystemRewriteOptions::Initialize();
    AddProperties();
  }
}

void NgxRewriteOptions::Terminate() {
  if (Properties::Terminate(&ngx_properties_)) {
    SystemRewriteOptions::Terminate();
  }
}

bool NgxRewriteOptions::IsDirective(StringPiece config_directive,
                                    StringPiece compare_directive) {
  return StringCaseEqual(config_directive, compare_directive);
}

RewriteOptions::OptionScope NgxRewriteOptions::GetOptionScope(
    StringPiece option_name) {
  ngx_uint_t i;
  ngx_uint_t size = sizeof(main_only_options) / sizeof(char*);
  for (i = 0; i < size; i++) {
    if (StringCaseEqual(main_only_options[i], option_name)) {
      return kProcessScopeStrict;
    }
  }

  size = sizeof(server_only_options) / sizeof(char*);
  for (i = 0; i < size; i++) {
    if (StringCaseEqual(server_only_options[i], option_name)) {
      return kServerScope;
    }
  }

  // This could be made more efficient if RewriteOptions provided a map allowing
  // access of options by their name. It's not too much of a worry at present
  // since this is just during initialization.
  for (OptionBaseVector::const_iterator it = all_options().begin();
       it != all_options().end(); ++it) {
    RewriteOptions::OptionBase* option = *it;
    if (option->option_name() == option_name) {
      // We treat kLegacyProcessScope as kProcessScopeStrict, failing to start
      // if an option is out of place.
      return option->scope() == kLegacyProcessScope ? kProcessScopeStrict
                                                    : option->scope();
    }
  }
  return kDirectoryScope;
}

RewriteOptions::OptionSettingResult NgxRewriteOptions::ParseAndSetOptions0(
    StringPiece directive, GoogleString* msg, MessageHandler* handler) {
  EnabledEnum enabled;
  if (!ParseFromString(directive, &enabled)) {
    return RewriteOptions::kOptionNameUnknown;
  }
  if (enabled == RewriteOptions::kEnabledOff) {
    // In ngx_pagespeed, for historical reasons, we treat "off" as "unplugged".
    // Also, "off" is deprecated and people should be using "standby" or
    // "unplugged" now depending on which sense they want.  See comment on
    // RewriteOptions::EnabledEnum.
    enabled = RewriteOptions::kEnabledUnplugged;
  }
  set_enabled(enabled);
  return RewriteOptions::kOptionOk;
}

RewriteOptions::OptionSettingResult
    NgxRewriteOptions::ParseAndSetOptionFromName1(
        StringPiece name, StringPiece arg,
        GoogleString* msg, MessageHandler* handler) {
  // FileCachePath needs error checking.
  if (StringCaseEqual(name, kFileCachePath)) {
    if (!StringCaseStartsWith(arg, "/")) {
      *msg = "must start with a slash";
      return RewriteOptions::kOptionValueInvalid;
    }
  }

  return SystemRewriteOptions::ParseAndSetOptionFromName1(
      name, arg, msg, handler);
}

template <class DriverFactoryT>
RewriteOptions::OptionSettingResult ParseAndSetOptionHelper(
    StringPiece option_value,
    DriverFactoryT* driver_factory,
    void (DriverFactoryT::*set_option_method)(bool)) {
  bool parsed_value;
  if (StringCaseEqual(option_value, "on") ||
      StringCaseEqual(option_value, "true")) {
    parsed_value = true;
  } else if (StringCaseEqual(option_value, "off") ||
             StringCaseEqual(option_value, "false")) {
    parsed_value = false;
  } else {
    return RewriteOptions::kOptionValueInvalid;
  }

  (driver_factory->*set_option_method)(parsed_value);
  return RewriteOptions::kOptionOk;
}

namespace {

const char* ps_error_string_for_option(
    ngx_pool_t* pool, StringPiece directive, StringPiece warning) {
  GoogleString msg =
      StrCat("\"", directive, "\" ", warning);
  char* s = string_piece_to_pool_string(pool, msg);
  if (s == NULL) {
    return "failed to allocate memory";
  }
  return s;
}

}  // namespace

// Very similar to apache/mod_instaweb::ParseDirective.
const char* NgxRewriteOptions::ParseAndSetOptions(
    StringPiece* args, int n_args, ngx_pool_t* pool, MessageHandler* handler,
    NgxRewriteDriverFactory* driver_factory,
    RewriteOptions::OptionScope scope, ngx_conf_t* cf,
    ProcessScriptVariablesMode script_mode) {
  CHECK_GE(n_args, 1);

  StringPiece directive = args[0];

  // Remove initial "ModPagespeed" if there is one.
  StringPiece mod_pagespeed("ModPagespeed");
  if (StringCaseStartsWith(directive, mod_pagespeed)) {
    directive.remove_prefix(mod_pagespeed.size());
  }

  if (GetOptionScope(directive) > scope) {
    return ps_error_string_for_option(
        pool, directive, "cannot be set at this scope.");
  }

  bool compile_scripts = false;

  if (script_mode != ProcessScriptVariablesMode::kOff) {
    // In the old mode we only allowed a few, so restrict to those.
    compile_scripts =
        StringCaseStartsWith(directive, "LoadFromFile") ||
        StringCaseEqual(directive, "EnableFilters") ||
        StringCaseEqual(directive, "DisableFilters") ||
        StringCaseEqual(directive, "DownstreamCachePurgeLocationPrefix") ||
        StringCaseEqual(directive, "DownstreamCachePurgeMethod") ||
        StringCaseEqual(directive,
                        "DownstreamCacheRewrittenPercentageThreshold") ||
        StringCaseEqual(directive, "ShardDomain");
    // In the new behaviour we also allow scripting of query- and directory-
    // scoped options.
    compile_scripts |=
        script_mode == ProcessScriptVariablesMode::kAll &&
        (GetOptionScope(directive) <= RewriteOptions::kDirectoryScope ||
         (StringCaseEqual(directive, "Allow") ||
          StringCaseEqual(directive, "BlockingRewriteRefererUrls") ||
          StringCaseEqual(directive, "Disallow") ||
          StringCaseEqual(directive, "DistributableFilters") ||
          StringCaseEqual(directive, "Domain") ||
          StringCaseEqual(directive, "ExperimentVariable") ||
          StringCaseEqual(directive, "ExperimentSpec") ||
          StringCaseEqual(directive, "ForbidFilters") ||
          StringCaseEqual(directive, "RetainComment") ||
          StringCaseEqual(directive, "CustomFetchHeader") ||
          StringCaseEqual(directive, "MapOriginDomain") ||
          StringCaseEqual(directive, "MapProxyDomain") ||
          StringCaseEqual(directive, "MapRewriteDomain") ||
          StringCaseEqual(directive, "UrlValuedAttribute") ||
          StringCaseEqual(directive, "Library")));
  }

  ScriptLine* script_line;
  script_line = NULL;

  if (n_args == 1 && StringCaseEqual(directive, "ClearInheritedScripts")) {
    clear_inherited_scripts_ = true;
    return NGX_CONF_OK;
  }

  if (compile_scripts) {
    CHECK(cf != NULL);
    int i;
    // Skip the first arg which is always 'pagespeed'
    for (i = 1; i < n_args; i++) {
      ngx_str_t script_source;

      script_source.len = args[i].as_string().length();
      std::string tmp = args[i].as_string();
      script_source.data = reinterpret_cast<u_char*>(
          const_cast<char*>(tmp.c_str()));

      if (ngx_http_script_variables_count(&script_source) > 0) {
        ngx_http_script_compile_t* sc =
            reinterpret_cast<ngx_http_script_compile_t*>(
                ngx_pcalloc(cf->pool, sizeof(ngx_http_script_compile_t)));
        sc->cf = cf;
        sc->source = &script_source;
        sc->lengths = reinterpret_cast<ngx_array_t**>(
            ngx_pcalloc(cf->pool, sizeof(ngx_array_t*)));
        sc->values = reinterpret_cast<ngx_array_t**>(
            ngx_pcalloc(cf->pool, sizeof(ngx_array_t*)));
        sc->variables = 1;
        sc->complete_lengths = 1;
        sc->complete_values = 1;
        if (ngx_http_script_compile(sc) != NGX_OK) {
          return ps_error_string_for_option(
              pool, directive, "Failed to compile script variables");
        } else {
          if (script_line == NULL) {
            script_line = new ScriptLine(args, n_args, scope);
          }
          script_line->AddScriptAndArgIndex(sc, i);
        }
      }
    }

    if (script_line != NULL) {
      script_lines_.push_back(RefCountedPtr<ScriptLine>(script_line));
      // We have found script variables in the current configuration line, and
      // prepared the associated rewriteoptions for that.
      // We will defer parsing, validation and processing of this line to
      // request time. That means we are done handling this configuration line.
      return NGX_CONF_OK;
    }
  }

  GoogleString msg;
  OptionSettingResult result;
  if (n_args == 1) {
    result = ParseAndSetOptions0(directive, &msg, handler);
  } else if (n_args == 2) {
    StringPiece arg = args[1];
    if (IsDirective(directive, "UseNativeFetcher")) {
      result = ParseAndSetOptionHelper<NgxRewriteDriverFactory>(
          arg, driver_factory,
          &NgxRewriteDriverFactory::set_use_native_fetcher);
    } else if (IsDirective(directive, "NativeFetcherMaxKeepaliveRequests")) {
      int max_keepalive_requests;
      if (StringToInt(arg, &max_keepalive_requests) &&
          max_keepalive_requests > 0) {
        driver_factory->set_native_fetcher_max_keepalive_requests(
            max_keepalive_requests);
        result = RewriteOptions::kOptionOk;
      } else {
        result = RewriteOptions::kOptionValueInvalid;
      }
    } else if (StringCaseEqual("ProcessScriptVariables", args[0])) {
      if (scope == RewriteOptions::kProcessScopeStrict) {
        ProcessScriptVariablesMode mode;
        if (StringCaseEqual(arg, "all")) {
          mode = ProcessScriptVariablesMode::kAll;
        } else if (StringCaseEqual(arg, "on")) {
          mode = ProcessScriptVariablesMode::kLegacyRestricted;
        } else if (StringCaseEqual(arg, "off")) {
          mode = ProcessScriptVariablesMode::kOff;
        } else {
          return const_cast<char*>(
              "pagespeed ProcessScriptVariables: invalid value");
        }
        if (driver_factory->SetProcessScriptVariables(mode)) {
          result = RewriteOptions::kOptionOk;
        } else {
          return const_cast<char*>(
              "pagespeed ProcessScriptVariables: can only be set once");
        }
      } else {
        return const_cast<char*>(
            "ProcessScriptVariables is only allowed at the top level");
      }
    } else {
      result = ParseAndSetOptionFromName1(directive, arg, &msg, handler);
      if (result == RewriteOptions::kOptionNameUnknown) {
        result = driver_factory->ParseAndSetOption1(
            directive,
            arg,
            scope >= RewriteOptions::kLegacyProcessScope,
            &msg,
            handler);
      }
    }
  } else if (n_args == 3) {
    result = ParseAndSetOptionFromName2(directive, args[1], args[2],
                                        &msg, handler);
    if (result == RewriteOptions::kOptionNameUnknown) {
      result = driver_factory->ParseAndSetOption2(
          directive,
          args[1],
          args[2],
          scope >= RewriteOptions::kLegacyProcessScope,
          &msg,
          handler);
    }
  } else if (n_args == 4) {
    result = ParseAndSetOptionFromName3(
        directive, args[1], args[2], args[3], &msg, handler);
  } else {
    result = RewriteOptions::kOptionNameUnknown;
  }

  switch (result) {
    case RewriteOptions::kOptionOk:
      return NGX_CONF_OK;
    case RewriteOptions::kOptionNameUnknown:
      return ps_error_string_for_option(
          pool, directive, "not recognized or too many arguments");
    case RewriteOptions::kOptionValueInvalid: {
      GoogleString full_directive;
      for (int i = 0 ; i < n_args ; i++) {
        StrAppend(&full_directive, i == 0 ? "" : " ", args[i]);
      }
      return ps_error_string_for_option(pool, full_directive, msg);
    }
  }

  CHECK(false);
  return NULL;
}

// Execute all entries in the script_lines vector, and hand the result off to
// ParseAndSetOptions to obtain the final option values.
bool NgxRewriteOptions::ExecuteScriptVariables(
    ngx_http_request_t* r, MessageHandler* handler,
    NgxRewriteDriverFactory* driver_factory) {
  bool script_error = false;

  if (script_lines_.size() > 0) {
    std::vector<RefCountedPtr<ScriptLine> >::iterator it;
    for (it = script_lines_.begin() ; it != script_lines_.end(); ++it) {
      ScriptLine* script_line = it->get();
      StringPiece args[NGX_PAGESPEED_MAX_ARGS];
      std::vector<ScriptArgIndex*>::iterator cs_it;
      int i;

      for (i = 0; i < script_line->n_args(); i++) {
        args[i] = script_line->args()[i];
      }

      for (cs_it = script_line->data().begin();
           cs_it != script_line->data().end(); cs_it++) {
        ngx_http_script_compile_t* script;
        ngx_array_t* values;
        ngx_array_t* lengths;
        ngx_str_t value;

        script = (*cs_it)->script();
        lengths = *script->lengths;
        values = *script->values;

        if (ngx_http_script_run(r, &value, lengths->elts, 0, values->elts)
            == NULL) {
          handler->Message(kError, "ngx_http_script_run error");
          script_error = true;
          break;
        } else  {
          args[(*cs_it)->index()] = str_to_string_piece(value);
        }
      }

      const char* status = ParseAndSetOptions(args, script_line->n_args(),
          r->pool, handler, driver_factory, script_line->scope(), NULL /*cf*/,
          ProcessScriptVariablesMode::kOff);

      if (status != NULL) {
        script_error = true;
        handler->Message(kWarning,
            "Error setting option value from script: '%s'", status);
        break;
      }
    }
  }

  if (script_error) {
    handler->Message(kWarning,
        "Script error(s) in configuration, disabling optimization");
    set_enabled(RewriteOptions::kEnabledOff);
    return false;
  }

  return true;
}

void NgxRewriteOptions::CopyScriptLinesTo(
    NgxRewriteOptions* destination) const {
  destination->script_lines_ = script_lines_;
}

void NgxRewriteOptions::AppendScriptLinesTo(
    NgxRewriteOptions* destination) const {
  destination->script_lines_.insert(destination->script_lines_.end(),
                                    script_lines_.begin(), script_lines_.end());
}

NgxRewriteOptions* NgxRewriteOptions::Clone() const {
  NgxRewriteOptions* options = new NgxRewriteOptions(
      StrCat("cloned from ", description()), thread_system());
  this->CopyScriptLinesTo(options);
  options->Merge(*this);
  return options;
}

const NgxRewriteOptions* NgxRewriteOptions::DynamicCast(
    const RewriteOptions* instance) {
  return dynamic_cast<const NgxRewriteOptions*>(instance);
}

NgxRewriteOptions* NgxRewriteOptions::DynamicCast(RewriteOptions* instance) {
  return dynamic_cast<NgxRewriteOptions*>(instance);
}

}  // namespace net_instaweb
