22#include <config_version.h>
30#include <svtools/strings.hrc>
37#include <officecfg/Office/Linguistic.hxx>
40#include <boost/property_tree/ptree.hpp>
41#include <boost/property_tree/json_parser.hpp>
47#include <com/sun/star/text/TextMarkupType.hpp>
48#include <com/sun/star/uno/Any.hxx>
51#include <osl/mutex.hxx>
61constexpr size_t MAX_SUGGESTIONS_SIZE = 10;
62using LanguageToolCfg = officecfg::Office::Linguistic::GrammarChecking::LanguageTool;
64PropertyValue lcl_GetLineColorPropertyFromErrorId(
const std::string& rErrorId)
67 if (rErrorId ==
"TYPOS" || rErrorId ==
"orth")
71 else if (rErrorId ==
"STYLE")
78 constexpr Color COL_ORANGE(0xD1, 0x68, 0x20);
84OString encodeTextForLanguageTool(
const OUString& text)
91 static constexpr auto myCharClass = rtl::createUriCharClass(
92 u8
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
94 rtl::Uri::encode(text, myCharClass.data(), rtl_UriEncodeStrict, RTL_TEXTENCODING_UTF8),
95 RTL_TEXTENCODING_ASCII_US);
99size_t WriteCallback(
void* ptr,
size_t size,
size_t nmemb,
void* userp)
104 std::string* response =
static_cast<std::string*
>(userp);
105 size_t real_size =
size * nmemb;
106 response->append(
static_cast<char*
>(ptr), real_size);
110enum class HTTP_METHOD
116std::string makeHttpRequest_impl(std::u16string_view aURL, HTTP_METHOD method,
117 const OString& aPostData, curl_slist* pHttpHeader,
120 struct curl_cleanup_t
122 void operator()(CURL* p)
const { curl_easy_cleanup(p); }
124 std::unique_ptr<CURL, curl_cleanup_t> curl(curl_easy_init());
127 SAL_WARN(
"languagetool",
"CURL initialization failed");
132 curl_version_info_data
const*
const pVersion(curl_version_info(CURLVERSION_NOW));
134 OString
const useragent(
135 OString::Concat(
"LibreOffice " LIBO_VERSION_DOTTED
" denylistedbackend/")
136 + pVersion->version +
" " + pVersion->ssl_version);
137 (void)curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, useragent.getStr());
140 (void)curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, pHttpHeader);
141 (void)curl_easy_setopt(curl.get(), CURLOPT_FAILONERROR, 1L);
142 (void)curl_easy_setopt(curl.get(), CURLOPT_URL, aURL8.getStr());
143 (void)curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 10L);
146 std::string response_body;
147 (void)curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallback);
148 (void)curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response_body);
151 if (!LanguageToolCfg::SSLCertVerify::get())
153 (void)curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER,
false);
154 (void)curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST,
false);
157 if (method == HTTP_METHOD::HTTP_POST)
159 (void)curl_easy_setopt(curl.get(), CURLOPT_POST, 1L);
160 (void)curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, aPostData.getStr());
163 CURLcode cc = curl_easy_perform(curl.get());
167 "CURL request returned with error: " <<
static_cast<sal_Int32
>(cc));
170 curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &nStatusCode);
171 return response_body;
174std::string makeDudenHttpRequest(std::u16string_view aURL,
const OString& aPostData,
177 struct curl_slist* pList =
nullptr;
179 =
OUStringToOString(LanguageToolCfg::ApiKey::get().value_or(
""), RTL_TEXTENCODING_UTF8);
181 pList = curl_slist_append(pList,
"Cache-Control: no-cache");
182 pList = curl_slist_append(pList,
"Content-Type: application/json");
183 if (!sAccessToken.isEmpty())
185 sAccessToken =
"access_token: " + sAccessToken;
186 pList = curl_slist_append(pList, sAccessToken.getStr());
189 return makeHttpRequest_impl(aURL, HTTP_METHOD::HTTP_POST, aPostData, pList, nStatusCode);
192std::string makeHttpRequest(std::u16string_view aURL, HTTP_METHOD method,
const OString& aPostData,
195 OString realPostData(aPostData);
196 if (method == HTTP_METHOD::HTTP_POST)
199 =
OUStringToOString(LanguageToolCfg::ApiKey::get().value_or(
""), RTL_TEXTENCODING_UTF8);
200 OString username =
OUStringToOString(LanguageToolCfg::Username::get().value_or(
""),
201 RTL_TEXTENCODING_UTF8);
202 if (!apiKey.isEmpty() && !username.isEmpty())
203 realPostData +=
"&username=" + username +
"&apiKey=" + apiKey;
206 return makeHttpRequest_impl(aURL, method, realPostData,
nullptr, nStatusCode);
209template <
typename Func>
210uno::Sequence<SingleProofreadingError> parseJson(std::string&& json, std::string path, Func f)
212 std::stringstream aStream(std::move(json));
213 boost::property_tree::ptree aRoot;
214 boost::property_tree::read_json(aStream, aRoot);
216 if (
auto tree = aRoot.get_child_optional(path))
218 uno::Sequence<SingleProofreadingError> aErrors(tree->size());
219 auto it = tree->begin();
220 for (
auto& rError : asNonConstRange(aErrors))
222 f(it->second, rError);
230void parseDudenResponse(ProofreadingResult& rResult, std::string&& aJSONBody)
232 rResult.aErrors = parseJson(
233 std::move(aJSONBody),
"check-positions",
234 [](
const boost::property_tree::ptree& rPos, SingleProofreadingError& rError) {
235 rError.nErrorStart = rPos.get<
int>(
"offset", 0);
236 rError.nErrorLength = rPos.get<
int>(
"length", 0);
237 rError.nErrorType = text::TextMarkupType::PROOFREADING;
240 const std::string
sType = rPos.get<std::string>(
"type", {});
241 rError.aProperties = { lcl_GetLineColorPropertyFromErrorId(sType) };
243 const auto proposals = rPos.get_child_optional(
"proposals");
246 rError.aSuggestions.realloc(std::min(proposals->size(), MAX_SUGGESTIONS_SIZE));
247 auto itProp = proposals->begin();
248 for (
auto& rSuggestion : asNonConstRange(rError.aSuggestions))
250 rSuggestion = OStringToOUString(itProp->second.data(), RTL_TEXTENCODING_UTF8);
260void parseProofreadingJSONResponse(ProofreadingResult& rResult, std::string&& aJSONBody)
262 rResult.aErrors = parseJson(
263 std::move(aJSONBody),
"matches",
264 [](
const boost::property_tree::ptree& match, SingleProofreadingError& rError) {
265 rError.nErrorStart =
match.get<
int>(
"offset", 0);
266 rError.nErrorLength =
match.get<
int>(
"length", 0);
267 rError.nErrorType = text::TextMarkupType::PROOFREADING;
268 const std::string shortMessage =
match.get<std::string>(
"message", {});
269 const std::string message =
match.get<std::string>(
"shortMessage", {});
271 rError.aShortComment = OStringToOUString(shortMessage, RTL_TEXTENCODING_UTF8);
272 rError.aFullComment = OStringToOUString(message, RTL_TEXTENCODING_UTF8);
275 std::string errorCategoryId;
276 if (
auto rule =
match.get_child_optional(
"rule"))
277 if (
auto ruleCategory = rule->get_child_optional(
"category"))
278 errorCategoryId = ruleCategory->get<std::string>(
"id", {});
279 rError.aProperties = { lcl_GetLineColorPropertyFromErrorId(errorCategoryId) };
281 const auto replacements =
match.get_child_optional(
"replacements");
287 rError.aSuggestions.realloc(std::min(replacements->size(), MAX_SUGGESTIONS_SIZE));
288 auto itRep = replacements->begin();
289 for (
auto& rSuggestion : asNonConstRange(rError.aSuggestions))
291 std::string replacementStr = itRep->second.get<std::string>(
"value", {});
292 rSuggestion = OStringToOUString(replacementStr, RTL_TEXTENCODING_UTF8);
298OUString getLocaleListURL()
300 if (
auto oURL = LanguageToolCfg::BaseURL::get())
301 if (!oURL->isEmpty())
302 return *oURL +
"/languages";
306OUString getCheckerURL()
308 if (
auto oURL = LanguageToolCfg::BaseURL::get())
309 if (!oURL->isEmpty())
310 return *oURL +
"/check";
330 if (rLocale == suppLocale)
341 if (!LanguageToolCfg::IsEnabled::get())
346 OUString localeUrl = getLocaleListURL();
347 if (localeUrl.isEmpty())
352 std::string response = makeHttpRequest(localeUrl, HTTP_METHOD::HTTP_GET, OString(), statusCode);
353 if (statusCode != 200)
357 if (response.empty())
361 boost::property_tree::ptree root;
362 std::stringstream aStream(response);
363 boost::property_tree::read_json(aStream, root);
365 size_t length = root.size();
369 for (
auto it = root.begin(); it != root.end(); it++,
i++)
371 boost::property_tree::ptree& localeItem = it->second;
372 const std::string longCode = localeItem.get<std::string>(
"longCode");
374 OUString(longCode.c_str(), longCode.length(), RTL_TEXTENCODING_UTF8));
381 const OUString& aDocumentIdentifier,
const OUString& aText,
const Locale& aLocale,
382 sal_Int32 nStartOfSentencePosition, sal_Int32 nSuggestedBehindEndOfSentencePosition,
383 const uno::Sequence<PropertyValue>& aProperties)
386 ProofreadingResult xRes;
387 xRes.aDocumentIdentifier = aDocumentIdentifier;
389 xRes.aLocale = aLocale;
390 xRes.nStartOfSentencePosition = nStartOfSentencePosition;
391 xRes.nBehindEndOfSentencePosition = nSuggestedBehindEndOfSentencePosition;
392 xRes.aProperties = {};
393 xRes.xProofreader =
this;
401 if (nStartOfSentencePosition != 0)
406 xRes.nStartOfNextSentencePosition = aText.getLength();
408 if (!LanguageToolCfg::IsEnabled::get())
413 OUString checkerURL = getCheckerURL();
414 if (checkerURL.isEmpty())
426 sal_Int32 spaceIndex = std::min(xRes.nStartOfNextSentencePosition, aText.getLength() - 1);
427 while (spaceIndex < aText.getLength() && aText[spaceIndex] ==
' ')
429 xRes.nStartOfNextSentencePosition += 1;
430 spaceIndex = xRes.nStartOfNextSentencePosition;
432 if (xRes.nStartOfNextSentencePosition == nSuggestedBehindEndOfSentencePosition
433 && spaceIndex < aText.getLength())
435 xRes.nStartOfNextSentencePosition
436 = std::min(nSuggestedBehindEndOfSentencePosition + 1, aText.getLength());
438 xRes.nBehindEndOfSentencePosition
439 = std::min(xRes.nStartOfNextSentencePosition, aText.getLength());
442 OString postData = encodeTextForLanguageTool(aText);
443 const bool bDudenProtocol = LanguageToolCfg::RestProtocol::get().value_or(
"") ==
"duden";
446 std::stringstream aStream;
447 boost::property_tree::ptree aTree;
448 aTree.put(
"text-language", langTag.getStr());
449 aTree.put(
"text", postData.getStr());
450 aTree.put(
"hyphenation",
false);
451 aTree.put(
"spellchecking-level", 3);
452 aTree.put(
"correction-proposals",
true);
453 boost::property_tree::write_json(aStream, aTree);
454 postData = OString(aStream.str());
458 postData =
"text=" + postData +
"&language=" + langTag;
463 xRes.aErrors = cachedResult->second;
468 std::string response_body;
470 response_body = makeDudenHttpRequest(checkerURL, postData, http_code);
472 response_body = makeHttpRequest(checkerURL, HTTP_METHOD::HTTP_POST, postData, http_code);
474 if (http_code != 200)
479 if (response_body.length() <= 0)
486 parseDudenResponse(xRes, std::move(response_body));
490 parseProofreadingJSONResponse(xRes, std::move(response_body));
512 return "org.openoffice.lingu.LanguageToolGrammarChecker";
527extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface*
529 css::uno::XComponentContext*, css::uno::Sequence<css::uno::Any>
const&)
PropertiesInfo aProperties
static css::lang::Locale convertToLocale(LanguageType nLangID, bool bResolveSystem=true)
static OUString convertToBcp47(LanguageType nLangID)
void insert(key_value_pair_t &rPair)
list_const_iterator_t end() const
list_const_iterator_t find(const Key &key)
constexpr ::Color COL_LIGHTRED(0xFF, 0x00, 0x00)
constexpr ::Color COL_LIGHTBLUE(0x00, 0x00, 0xFF)
#define SAL_WARN(area, stream)
#define SAL_INFO(area, stream)
constexpr OUStringLiteral SN_GRAMMARCHECKER
std::locale Create(std::string_view aPrefixName, const LanguageTag &rLocale)
OUString get(TranslateId sContextAndId, const std::locale &loc)
css::beans::PropertyValue makePropertyValue(const OUString &rName, T &&rValue)
bool match(const sal_Unicode *pWild, const sal_Unicode *pStr, const sal_Unicode cEscape)
bool CPPUHELPER_DLLPUBLIC supportsService(css::lang::XServiceInfo *implementation, rtl::OUString const &name)
OString OUStringToOString(std::u16string_view str, ConnectionSettings const *settings)