| // Copyright 2010-2014, Google Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #include "net/http_client.h" |
| |
| |
| #ifdef MOZC_ENABLE_HTTP_CLIENT |
| #if defined(OS_WIN) |
| #include <windows.h> |
| #include <wininet.h> |
| #elif defined(OS_ANDROID) |
| #include "base/android_jni_proxy.h" |
| #elif defined(HAVE_CURL) |
| #include <curl/curl.h> |
| #endif |
| #endif // MOZC_ENABLE_HTTP_CLIENT |
| |
| #include "base/compiler_specific.h" |
| #include "base/logging.h" |
| #include "base/port.h" |
| #include "base/singleton.h" |
| #include "base/stopwatch.h" |
| #include "base/util.h" |
| #include "net/http_client_common.h" |
| #ifdef MOZC_ENABLE_HTTP_CLIENT |
| #include "net/http_client_mac.h" |
| #include "net/http_client_pepper.h" |
| #include "net/proxy_manager.h" |
| #endif // MOZC_ENABLE_HTTP_CLIENT |
| |
| namespace mozc { |
| // We use a dummy user agent. |
| const char *kUserAgent = "Mozilla/5.0"; |
| const int kOKResponseCode = 200; |
| |
| namespace { |
| #if defined(MOZC_ENABLE_HTTP_CLIENT) |
| class HTTPStream { |
| public: |
| HTTPStream(string *output_string, size_t max_data_size) |
| : output_string_(output_string), |
| max_data_size_(max_data_size), |
| output_size_(0) { |
| if (NULL != output_string_) { |
| output_string_->clear(); |
| } |
| VLOG(2) << "max_data_size=" << max_data_size; |
| } |
| |
| virtual ~HTTPStream() { |
| VLOG(2) << output_size_ << " bytes received"; |
| } |
| |
| size_t Append(const char *buf, size_t size) { |
| if (output_size_ + size >= max_data_size_) { |
| size = (max_data_size_ - output_size_); |
| LOG(WARNING) << "too long data max_data_size=" << max_data_size_; |
| } |
| |
| if (output_string_ != NULL) { |
| VLOG(2) << "Recived: " << size << " bytes to std::string"; |
| output_string_->append(buf, size); |
| } |
| |
| output_size_ += size; |
| |
| return size; |
| } |
| |
| private: |
| string *output_string_; |
| size_t max_data_size_; |
| size_t output_size_; |
| }; |
| |
| #ifdef OS_WIN |
| // RAII class for HINTERNET |
| class ScopedHINTERNET { |
| public: |
| explicit ScopedHINTERNET(HINTERNET internet) |
| : internet_(internet) {} |
| virtual ~ScopedHINTERNET() { |
| if (NULL != internet_) { |
| VLOG(2) << "InternetCloseHandle() called"; |
| ::InternetCloseHandle(internet_); |
| } |
| internet_ = NULL; |
| } |
| |
| HINTERNET get() { |
| return internet_; |
| } |
| |
| private: |
| HINTERNET internet_; |
| }; |
| |
| void CALLBACK StatusCallback(HINTERNET internet, |
| DWORD_PTR context, |
| DWORD status, |
| LPVOID status_info, |
| DWORD status_info_size) { |
| if (status == INTERNET_STATUS_REQUEST_COMPLETE) { |
| ::SetEvent(reinterpret_cast<HANDLE>(context)); |
| } |
| if (status == INTERNET_STATUS_HANDLE_CLOSING) { |
| ::CloseHandle(reinterpret_cast<HANDLE>(context)); |
| } |
| } |
| |
| bool CheckTimeout(HANDLE event, int64 elapsed_msec, int32 timeout_msec) { |
| const DWORD error = ::GetLastError(); |
| if (error != ERROR_IO_PENDING) { |
| LOG(ERROR) << "Unexpected error state: " << error; |
| return false; |
| } |
| const int64 time_left = timeout_msec - elapsed_msec; |
| if (time_left < 0) { |
| LOG(WARNING) << "Already timed-out: " << time_left; |
| return false; |
| } |
| DCHECK_GE(elapsed_msec, 0); |
| DCHECK_LE(time_left, static_cast<int64>(MAXDWORD)) |
| << "This should always be true because |timeout_msec| <= MAXDWORD"; |
| const DWORD positive_time_left = static_cast<DWORD>(time_left); |
| const DWORD wait_result = ::WaitForSingleObject(event, positive_time_left); |
| if (wait_result == WAIT_FAILED) { |
| const DWORD wait_error = ::GetLastError(); |
| LOG(ERROR) << "WaitForSingleObject failed. error: " << wait_error; |
| return false; |
| } |
| if (wait_result == WAIT_TIMEOUT) { |
| LOG(WARNING) << "WaitForSingleObject timed out after " |
| << positive_time_left << " msec."; |
| return false; |
| } |
| if (wait_result != WAIT_OBJECT_0) { |
| LOG(ERROR) << "WaitForSingleObject returned unexpected result: " |
| << wait_result; |
| return false; |
| } |
| ::ResetEvent(event); |
| return true; |
| } |
| |
| bool RequestInternal(HTTPMethodType type, |
| const string &url, |
| const char *post_data, |
| size_t post_size, |
| const HTTPClient::Option &option, |
| string *output_string) { |
| if (option.timeout <= 0) { |
| LOG(ERROR) << "timeout should not be negative nor 0"; |
| return false; |
| } |
| |
| Stopwatch stopwatch = stopwatch.StartNew(); |
| |
| HANDLE event = ::CreateEvent(NULL, FALSE, FALSE, NULL); |
| if (NULL == event) { |
| LOG(ERROR) << "CreateEvent() failed: " << ::GetLastError(); |
| return false; |
| } |
| |
| ScopedHINTERNET internet(::InternetOpenA(kUserAgent, |
| INTERNET_OPEN_TYPE_PRECONFIG, |
| NULL, |
| NULL, |
| INTERNET_FLAG_ASYNC)); |
| |
| if (NULL == internet.get()) { |
| LOG(ERROR) << "InternetOpen() failed: " |
| << ::GetLastError() << " " << url; |
| ::CloseHandle(event); |
| return false; |
| } |
| |
| ::InternetSetStatusCallback(internet.get(), StatusCallback); |
| |
| URL_COMPONENTSW uc; |
| wchar_t Scheme[128]; |
| wchar_t HostName[512]; |
| wchar_t UserName[64]; |
| wchar_t Password[64]; |
| wchar_t UrlPath[256]; |
| wchar_t ExtraInfo[512]; |
| |
| uc.dwStructSize = sizeof(uc); |
| uc.lpszScheme = Scheme; |
| uc.lpszHostName = HostName; |
| uc.lpszUserName = UserName; |
| uc.lpszPassword = Password; |
| uc.lpszUrlPath = UrlPath; |
| uc.lpszExtraInfo = ExtraInfo; |
| |
| uc.dwSchemeLength = sizeof(Scheme); |
| uc.dwHostNameLength = sizeof(HostName); |
| uc.dwUserNameLength = sizeof(UserName); |
| uc.dwPasswordLength = sizeof(Password); |
| uc.dwUrlPathLength = sizeof(UrlPath); |
| uc.dwExtraInfoLength = sizeof(ExtraInfo); |
| |
| wstring wurl; |
| Util::UTF8ToWide(url.c_str(), &wurl); |
| |
| if (!::InternetCrackUrlW(wurl.c_str(), 0, 0, &uc)) { |
| LOG(WARNING) << "InternetCrackUrl() failed: " |
| << ::GetLastError() << " " << url; |
| return false; |
| } |
| |
| if (uc.nScheme != INTERNET_SCHEME_HTTP && |
| uc.nScheme != INTERNET_SCHEME_HTTPS) { |
| LOG(WARNING) << "Only accept HTTP or HTTPS: " << url; |
| return false; |
| } |
| |
| ScopedHINTERNET session(::InternetConnect(internet.get(), |
| uc.lpszHostName, |
| uc.nPort, |
| NULL, |
| NULL, |
| INTERNET_SERVICE_HTTP, |
| 0, |
| 0)); |
| |
| if (NULL == session.get()) { |
| LOG(ERROR) << "InternetConnect() failed: " |
| << ::GetLastError() << " " << url; |
| return false; |
| } |
| |
| wstring uri = UrlPath; |
| if (uc.dwExtraInfoLength != 0) { |
| uri += uc.lpszExtraInfo; |
| } |
| |
| const wchar_t *method_type_string[] |
| = { L"GET", L"HEAD", L"POST", L"PUT", L"DELETE" }; |
| const wchar_t *method = method_type_string[static_cast<int>(type)]; |
| CHECK(method); |
| |
| ScopedHINTERNET handle(::HttpOpenRequestW |
| (session.get(), |
| method, |
| uri.c_str(), |
| NULL, NULL, NULL, |
| INTERNET_FLAG_RELOAD | |
| INTERNET_FLAG_DONT_CACHE | |
| INTERNET_FLAG_NO_UI | |
| INTERNET_FLAG_PRAGMA_NOCACHE | |
| (uc.nScheme == INTERNET_SCHEME_HTTPS ? |
| INTERNET_FLAG_SECURE : 0), |
| reinterpret_cast<DWORD_PTR>(event))); |
| |
| if (NULL == handle.get()) { |
| LOG(ERROR) << "HttpOpenRequest() failed: " |
| << ::GetLastError() << " " << url; |
| return false; |
| } |
| |
| for (size_t i = 0; i < option.headers.size(); ++i) { |
| const string header = option.headers[i] + "\r\n"; |
| wstring wheader; |
| Util::UTF8ToWide(header.c_str(), &wheader); |
| if (!::HttpAddRequestHeadersW(handle.get(), wheader.c_str(), -1, |
| HTTP_ADDREQ_FLAG_ADD | |
| HTTP_ADDREQ_FLAG_REPLACE)) { |
| LOG(WARNING) << "HttpAddRequestHeaders() failed: " << option.headers[i] |
| << " " << ::GetLastError(); |
| return false; |
| } |
| } |
| |
| if (!::HttpSendRequest(handle.get(), |
| NULL, 0, |
| (type == HTTP_POST) ? (LPVOID)post_data : NULL, |
| (type == HTTP_POST) ? post_size : 0)) { |
| if (!CheckTimeout(event, stopwatch.GetElapsedMilliseconds(), |
| option.timeout)) { |
| LOG(ERROR) << "HttpSendRequest() failed: " |
| << ::GetLastError() << " " << url; |
| return false; |
| } |
| } else { |
| return false; |
| } |
| |
| if (VLOG_IS_ON(2)) { |
| char buf[8192]; |
| DWORD size = sizeof(buf); |
| if (::HttpQueryInfoA(handle.get(), |
| HTTP_QUERY_RAW_HEADERS_CRLF | |
| HTTP_QUERY_FLAG_REQUEST_HEADERS, |
| buf, |
| &size, |
| 0)) { |
| LOG(INFO) << "Request Header: " << buf; |
| } |
| } |
| |
| DWORD code = 0; |
| { |
| DWORD code_size = sizeof(code); |
| if (!::HttpQueryInfoW(handle.get(), |
| HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, |
| &code, |
| &code_size, |
| 0)) { |
| LOG(ERROR) << "HttpQueryInfo() failed: " |
| << ::GetLastError() << " " << url; |
| return false; |
| } |
| } |
| |
| // make stream |
| HTTPStream stream(output_string, option.max_data_size); |
| |
| if (option.include_header || type == HTTP_HEAD) { |
| char buf[8192]; |
| DWORD buf_size = sizeof(buf); |
| if (!::HttpQueryInfoA(handle.get(), |
| HTTP_QUERY_RAW_HEADERS_CRLF, |
| buf, |
| &buf_size, |
| 0)) { |
| LOG(ERROR) << "HttpQueryInfo() failed: " |
| << ::GetLastError() << " " << url; |
| return false; |
| } |
| |
| if (buf_size != stream.Append(buf, buf_size)) { |
| return false; |
| } |
| |
| if (type == HTTP_HEAD) { |
| return true; |
| } |
| } |
| |
| if (type == HTTP_POST || type == HTTP_GET) { |
| char buf[8129]; |
| INTERNET_BUFFERSA ibuf; |
| ::ZeroMemory(&ibuf, sizeof(ibuf)); |
| ibuf.dwStructSize = sizeof(ibuf); |
| while (true) { |
| ibuf.lpvBuffer = buf; |
| ibuf.dwBufferLength = sizeof(buf); |
| if (::InternetReadFileExA(handle.get(), |
| &ibuf, |
| WININET_API_FLAG_ASYNC, |
| reinterpret_cast<DWORD_PTR>(event)) || |
| CheckTimeout(event, stopwatch.GetElapsedMilliseconds(), |
| option.timeout)) { |
| const DWORD size = ibuf.dwBufferLength; |
| if (size == 0) { |
| break; |
| } |
| if (size != stream.Append(buf, size)) { |
| return false; |
| } |
| } else { |
| LOG(ERROR) << "InternetReadFileEx() failed: " << ::GetLastError(); |
| return false; |
| } |
| } |
| } |
| |
| if (kOKResponseCode != code) { |
| LOG(WARNING) << "status code is not 200: " << code << " " << url; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| #elif defined(OS_MACOSX) |
| |
| bool RequestInternal(HTTPMethodType type, |
| const string &url, |
| const char *post_data, |
| size_t post_size, |
| const HTTPClient::Option &option, |
| string *output_string) { |
| return MacHTTPRequestHandler::Request(type, url, |
| post_data, post_size, option, |
| output_string); |
| } |
| |
| #elif defined(__native_client__) |
| |
| bool RequestInternal(HTTPMethodType type, |
| const string &url, |
| const char *post_data, |
| size_t post_size, |
| const HTTPClient::Option &option, |
| string *output_string) { |
| return PepperHTTPRequestHandler::Request(type, url, |
| post_data, post_size, option, |
| output_string); |
| } |
| |
| #elif defined(OS_ANDROID) |
| bool RequestInternal(HTTPMethodType type, |
| const string &url, |
| const char *post_data, |
| size_t post_size, |
| const HTTPClient::Option &option, |
| string *output_string) { |
| // TODO(matsuzakit): Put body field in HTTP response on |output_string| |
| // if the request arrives to the server and fails. |
| return jni::JavaHttpClientProxy::Request(type, url, post_data, post_size, |
| option, output_string); |
| } |
| #elif defined(HAVE_CURL) |
| |
| class CurlInitializer { |
| public: |
| CurlInitializer() { |
| curl_global_init(CURL_GLOBAL_ALL); |
| } |
| |
| ~CurlInitializer() { |
| curl_global_cleanup(); |
| } |
| |
| void Init() {} |
| }; |
| |
| size_t HTTPOutputCallback(void *ptr, size_t size, size_t nmemb, void *stream) { |
| HTTPStream *s = reinterpret_cast<HTTPStream *>(stream); |
| return s->Append(reinterpret_cast<const char *>(ptr), size * nmemb); |
| } |
| |
| int HTTPDebugCallback(CURL *curl, curl_infotype type, |
| char *buf, size_t size, void *data) { |
| if (CURLINFO_TEXT != type) { |
| return 0; |
| } |
| string *output = reinterpret_cast<string *>(data); |
| output->append(buf, size); |
| return 0; |
| } |
| |
| bool RequestInternal(HTTPMethodType type, |
| const string &url, |
| const char *post_data, |
| size_t post_size, |
| const HTTPClient::Option &option, |
| string *output_string) { |
| if (option.timeout < 0) { |
| LOG(ERROR) << "timeout should not be negative nor 0"; |
| return false; |
| } |
| |
| Singleton<CurlInitializer>::get()->Init(); |
| |
| CURL *curl = curl_easy_init(); |
| if (NULL == curl) { |
| LOG(ERROR) << "curl_easy_init() failed"; |
| return false; |
| } |
| |
| HTTPStream stream(output_string, option.max_data_size); |
| |
| string debug; |
| if (VLOG_IS_ON(2)) { |
| curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); |
| curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, HTTPDebugCallback); |
| curl_easy_setopt(curl, CURLOPT_DEBUGDATA, reinterpret_cast<void *>(&debug)); |
| } |
| |
| curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); |
| curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1); |
| curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); |
| curl_easy_setopt(curl, CURLOPT_AUTOREFERER, 1); |
| curl_easy_setopt(curl, CURLOPT_USERAGENT, kUserAgent); |
| curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1); |
| curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, option.timeout); |
| curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, option.timeout); |
| curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5); |
| curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &HTTPOutputCallback); |
| curl_easy_setopt(curl, CURLOPT_WRITEDATA, |
| reinterpret_cast<void *>(&stream)); |
| |
| string proxy_host; |
| string proxy_auth; |
| if (ProxyManager::GetProxyData(url, &proxy_host, &proxy_auth)) { |
| curl_easy_setopt(curl, CURLOPT_PROXY, proxy_host.c_str()); |
| if (!proxy_auth.empty()) { |
| curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, proxy_auth.c_str()); |
| } |
| } |
| |
| struct curl_slist *slist = NULL; |
| for (size_t i = 0; i < option.headers.size(); ++i) { |
| VLOG(2) << "Add header: " << option.headers[i]; |
| slist = curl_slist_append(slist, option.headers[i].c_str()); |
| } |
| |
| if (slist != NULL) { |
| curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); |
| } |
| |
| if (option.include_header) { |
| curl_easy_setopt(curl, CURLOPT_HEADER, 1); |
| } |
| |
| switch (type) { |
| case HTTP_GET: |
| curl_easy_setopt(curl, CURLOPT_HTTPGET, 1); |
| break; |
| case HTTP_POST: |
| curl_easy_setopt(curl, CURLOPT_HTTPPOST, 1); |
| curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data); |
| curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post_size); |
| break; |
| case HTTP_HEAD: |
| curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "HEAD"); |
| curl_easy_setopt(curl, CURLOPT_NOBODY, 1); |
| curl_easy_setopt(curl, CURLOPT_HEADER, 1); |
| break; |
| default: |
| LOG(ERROR) << "Unknown method: " << type; |
| curl_easy_cleanup(curl); |
| return false; |
| break; |
| } |
| |
| const CURLcode ret = curl_easy_perform(curl); |
| bool result = (CURLE_OK == ret); |
| |
| if (!result) { |
| LOG(WARNING) << "curl_easy_perform() failed: " << curl_easy_strerror(ret); |
| } |
| |
| int code = 0; |
| curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); |
| if (kOKResponseCode != code) { |
| LOG(WARNING) << "status code is not 200: " << code; |
| result = false; |
| } |
| |
| curl_easy_cleanup(curl); |
| if (slist != NULL) { |
| curl_slist_free_all(slist); |
| } |
| |
| if (VLOG_IS_ON(2)) { |
| LOG(INFO) << debug; |
| } |
| |
| return result; |
| } |
| #else // defined(HAVE_CURL) |
| // None of OS_WIN/OS_MACOSX/HAVE_CURL is defined. |
| #error "HttpClient does not support your platform." |
| #endif // defined(HAVE_CURL) |
| #else // defined(MOZC_ENABLE_HTTP_CLIENT) |
| // MOZC_ENABLE_HTTP_CLIENT is not defined |
| bool RequestInternal(HTTPMethodType type, |
| const string &url, |
| const char *post_data, |
| size_t post_size, |
| const HTTPClient::Option &option, |
| string *output_string) { |
| // Null implementation. |
| LOG(ERROR) << "HttpClient is not enabled."; |
| return false; |
| } |
| #endif // MOZC_ENABLE_HTTP_CLIENT |
| |
| } // namespace |
| |
| class HTTPClientImpl: public HTTPClientInterface { |
| public: |
| virtual bool Get(const string &url, const HTTPClient::Option &option, |
| string *output_string) const { |
| return RequestInternal(HTTP_GET, url, NULL, 0, option, |
| output_string); |
| } |
| |
| virtual bool Head(const string &url, const HTTPClient::Option &option, |
| string *output_string) const { |
| return RequestInternal(HTTP_HEAD, url, NULL, 0, option, |
| output_string); |
| } |
| |
| virtual bool Post(const string &url, const string &data, |
| const HTTPClient::Option &option, |
| string *output_string) const { |
| return RequestInternal(HTTP_POST, url, data.data(), data.size(), |
| option, output_string); |
| } |
| }; |
| |
| namespace { |
| |
| const HTTPClientInterface *g_http_connection_handler = NULL; |
| |
| const HTTPClientInterface &GetHTTPClient() { |
| if (g_http_connection_handler == NULL) { |
| g_http_connection_handler = Singleton<HTTPClientImpl>::get(); |
| } |
| return *g_http_connection_handler; |
| } |
| } // namespace |
| |
| void HTTPClient::SetHTTPClientHandler( |
| const HTTPClientInterface *handler) { |
| g_http_connection_handler = handler; |
| } |
| |
| bool HTTPClient::Get(const string &url, string *output_string) { |
| return GetHTTPClient().Get(url, Option(), output_string); |
| } |
| |
| bool HTTPClient::Head(const string &url, string *output_string) { |
| return GetHTTPClient().Head(url, Option(), output_string); |
| } |
| |
| bool HTTPClient::Post(const string &url, const string &data, |
| string *output_string) { |
| return GetHTTPClient().Post(url, data, Option(), output_string); |
| } |
| |
| bool HTTPClient::Get(const string &url, const Option &option, |
| string *output_string) { |
| return GetHTTPClient().Get(url, option, output_string); |
| } |
| |
| bool HTTPClient::Head(const string &url, const Option &option, |
| string *output_string) { |
| return GetHTTPClient().Head(url, option, output_string); |
| } |
| |
| bool HTTPClient::Post(const string &url, const string &data, |
| const Option &option, string *output_string) { |
| return GetHTTPClient().Post(url, data, option, output_string); |
| } |
| } // namespace mozc |