| // Copyright 2016 PDFium Authors. All rights reserved. | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com | 
 |  | 
 | #include "core/fpdftext/cpdf_linkextract.h" | 
 |  | 
 | #include <vector> | 
 |  | 
 | #include "core/fpdftext/cpdf_textpage.h" | 
 | #include "core/fxcrt/fx_extension.h" | 
 | #include "core/fxcrt/fx_string.h" | 
 | #include "core/fxcrt/fx_system.h" | 
 |  | 
 | namespace { | 
 |  | 
 | // Find the end of a web link starting from offset |start| and ending at offset | 
 | // |end|. The purpose of this function is to separate url from the surrounding | 
 | // context characters, we do not intend to fully validate the url. |str| | 
 | // contains lower case characters only. | 
 | size_t FindWebLinkEnding(const WideString& str, size_t start, size_t end) { | 
 |   if (str.Contains(L'/', start)) { | 
 |     // When there is a path and query after '/', most ASCII chars are allowed. | 
 |     // We don't sanitize in this case. | 
 |     return end; | 
 |   } | 
 |  | 
 |   // When there is no path, it only has IP address or host name. | 
 |   // Port is optional at the end. | 
 |   if (str[start] == L'[') { | 
 |     // IPv6 reference. | 
 |     // Find the end of the reference. | 
 |     auto result = str.Find(L']', start + 1); | 
 |     if (result.has_value()) { | 
 |       end = result.value(); | 
 |       if (end > start + 1) {  // Has content inside brackets. | 
 |         size_t len = str.GetLength(); | 
 |         size_t off = end + 1; | 
 |         if (off < len && str[off] == L':') { | 
 |           off++; | 
 |           while (off < len && FXSYS_IsDecimalDigit(str[off])) | 
 |             off++; | 
 |           if (off > end + 2 && | 
 |               off <= len)   // At least one digit in port number. | 
 |             end = off - 1;  // |off| is offset of the first invalid char. | 
 |         } | 
 |       } | 
 |     } | 
 |     return end; | 
 |   } | 
 |  | 
 |   // According to RFC1123, host name only has alphanumeric chars, hyphens, | 
 |   // and periods. Hyphen should not at the end though. | 
 |   // Non-ASCII chars are ignored during checking. | 
 |   while (end > start && str[end] < 0x80) { | 
 |     if (FXSYS_IsDecimalDigit(str[end]) || | 
 |         (str[end] >= L'a' && str[end] <= L'z') || str[end] == L'.') { | 
 |       break; | 
 |     } | 
 |     end--; | 
 |   } | 
 |   return end; | 
 | } | 
 |  | 
 | // Remove characters from the end of |str|, delimited by |start| and |end|, up | 
 | // to and including |charToFind|. No-op if |charToFind| is not present. Updates | 
 | // |end| if characters were removed. | 
 | void TrimBackwardsToChar(const WideString& str, | 
 |                          wchar_t charToFind, | 
 |                          size_t start, | 
 |                          size_t* end) { | 
 |   for (size_t pos = *end; pos >= start; pos--) { | 
 |     if (str[pos] == charToFind) { | 
 |       *end = pos - 1; | 
 |       break; | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | // Finds opening brackets ()[]{}<> and quotes "'  before the URL delimited by | 
 | // |start| and |end| in |str|. Matches a closing bracket or quote for each | 
 | // opening character and, if present, removes everything afterwards. Returns the | 
 | // new end position for the string. | 
 | size_t TrimExternalBracketsFromWebLink(const WideString& str, | 
 |                                        size_t start, | 
 |                                        size_t end) { | 
 |   for (size_t pos = 0; pos < start; pos++) { | 
 |     if (str[pos] == '(') { | 
 |       TrimBackwardsToChar(str, ')', start, &end); | 
 |     } else if (str[pos] == '[') { | 
 |       TrimBackwardsToChar(str, ']', start, &end); | 
 |     } else if (str[pos] == '{') { | 
 |       TrimBackwardsToChar(str, '}', start, &end); | 
 |     } else if (str[pos] == '<') { | 
 |       TrimBackwardsToChar(str, '>', start, &end); | 
 |     } else if (str[pos] == '"') { | 
 |       TrimBackwardsToChar(str, '"', start, &end); | 
 |     } else if (str[pos] == '\'') { | 
 |       TrimBackwardsToChar(str, '\'', start, &end); | 
 |     } | 
 |   } | 
 |   return end; | 
 | } | 
 |  | 
 | }  // namespace | 
 |  | 
 | CPDF_LinkExtract::CPDF_LinkExtract(const CPDF_TextPage* pTextPage) | 
 |     : m_pTextPage(pTextPage) {} | 
 |  | 
 | CPDF_LinkExtract::~CPDF_LinkExtract() = default; | 
 |  | 
 | void CPDF_LinkExtract::ExtractLinks() { | 
 |   m_LinkArray.clear(); | 
 |   size_t start = 0; | 
 |   size_t pos = 0; | 
 |   bool bAfterHyphen = false; | 
 |   bool bLineBreak = false; | 
 |   const size_t nTotalChar = m_pTextPage->CountChars(); | 
 |   const WideString page_text = m_pTextPage->GetAllPageText(); | 
 |   while (pos < nTotalChar) { | 
 |     const CPDF_TextPage::CharInfo& char_info = m_pTextPage->GetCharInfo(pos); | 
 |     if (char_info.m_CharType != CPDF_TextPage::CharType::kGenerated && | 
 |         char_info.m_Unicode != L' ' && pos != nTotalChar - 1) { | 
 |       bAfterHyphen = | 
 |           (char_info.m_CharType == CPDF_TextPage::CharType::kHyphen || | 
 |            (char_info.m_CharType == CPDF_TextPage::CharType::kNormal && | 
 |             char_info.m_Unicode == L'-')); | 
 |       ++pos; | 
 |       continue; | 
 |     } | 
 |  | 
 |     size_t nCount = pos - start; | 
 |     if (pos == nTotalChar - 1) { | 
 |       ++nCount; | 
 |     } else if (bAfterHyphen && | 
 |                (char_info.m_Unicode == L'\n' || char_info.m_Unicode == L'\r')) { | 
 |       // Handle text breaks with a hyphen to the next line. | 
 |       bLineBreak = true; | 
 |       ++pos; | 
 |       continue; | 
 |     } | 
 |  | 
 |     WideString strBeCheck = page_text.Substr(start, nCount); | 
 |     if (bLineBreak) { | 
 |       strBeCheck.Remove(L'\n'); | 
 |       strBeCheck.Remove(L'\r'); | 
 |       bLineBreak = false; | 
 |     } | 
 |     // Replace the generated code with the hyphen char. | 
 |     strBeCheck.Replace(L"\xfffe", L"-"); | 
 |  | 
 |     if (strBeCheck.GetLength() > 5) { | 
 |       while (strBeCheck.GetLength() > 0) { | 
 |         wchar_t ch = strBeCheck.Back(); | 
 |         if (ch != L')' && ch != L',' && ch != L'>' && ch != L'.') | 
 |           break; | 
 |  | 
 |         strBeCheck = strBeCheck.First(strBeCheck.GetLength() - 1); | 
 |         nCount--; | 
 |       } | 
 |  | 
 |       // Check for potential web URLs and email addresses. | 
 |       // Ftp address, file system links, data, blob etc. are not checked. | 
 |       if (nCount > 5) { | 
 |         auto maybe_link = CheckWebLink(strBeCheck); | 
 |         if (maybe_link.has_value()) { | 
 |           maybe_link.value().m_Start += start; | 
 |           m_LinkArray.push_back(maybe_link.value()); | 
 |         } else if (CheckMailLink(&strBeCheck)) { | 
 |           m_LinkArray.push_back(Link{{start, nCount}, strBeCheck}); | 
 |         } | 
 |       } | 
 |     } | 
 |     start = ++pos; | 
 |   } | 
 | } | 
 |  | 
 | absl::optional<CPDF_LinkExtract::Link> CPDF_LinkExtract::CheckWebLink( | 
 |     const WideString& strBeCheck) { | 
 |   static const wchar_t kHttpScheme[] = L"http"; | 
 |   static const wchar_t kWWWAddrStart[] = L"www."; | 
 |  | 
 |   const size_t kHttpSchemeLen = FXSYS_len(kHttpScheme); | 
 |   const size_t kWWWAddrStartLen = FXSYS_len(kWWWAddrStart); | 
 |  | 
 |   WideString str = strBeCheck; | 
 |   str.MakeLower(); | 
 |  | 
 |   // First, try to find the scheme. | 
 |   auto start = str.Find(kHttpScheme); | 
 |   if (start.has_value()) { | 
 |     size_t off = start.value() + kHttpSchemeLen;  // move after "http". | 
 |     if (str.GetLength() > off + 4) {  // At least "://<char>" follows. | 
 |       if (str[off] == L's')  // "https" scheme is accepted. | 
 |         off++; | 
 |       if (str[off] == L':' && str[off + 1] == L'/' && str[off + 2] == L'/') { | 
 |         off += 3; | 
 |         const size_t end = | 
 |             FindWebLinkEnding(str, off, | 
 |                               TrimExternalBracketsFromWebLink( | 
 |                                   str, start.value(), str.GetLength() - 1)); | 
 |         if (end > off) {  // Non-empty host name. | 
 |           const size_t nStart = start.value(); | 
 |           const size_t nCount = end - nStart + 1; | 
 |           return Link{{nStart, nCount}, strBeCheck.Substr(nStart, nCount)}; | 
 |         } | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   // When there is no scheme, try to find url starting with "www.". | 
 |   start = str.Find(kWWWAddrStart); | 
 |   if (start.has_value()) { | 
 |     size_t off = start.value() + kWWWAddrStartLen; | 
 |     if (str.GetLength() > off) { | 
 |       const size_t end = | 
 |           FindWebLinkEnding(str, start.value(), | 
 |                             TrimExternalBracketsFromWebLink( | 
 |                                 str, start.value(), str.GetLength() - 1)); | 
 |       if (end > off) { | 
 |         const size_t nStart = start.value(); | 
 |         const size_t nCount = end - nStart + 1; | 
 |         return Link{{nStart, nCount}, | 
 |                     L"http://" + strBeCheck.Substr(nStart, nCount)}; | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   return absl::nullopt; | 
 | } | 
 |  | 
 | bool CPDF_LinkExtract::CheckMailLink(WideString* str) { | 
 |   auto aPos = str->Find(L'@'); | 
 |   // Invalid when no '@' or when starts/ends with '@'. | 
 |   if (!aPos.has_value() || aPos.value() == 0 || aPos == str->GetLength() - 1) | 
 |     return false; | 
 |  | 
 |   // Check the local part. | 
 |   size_t pPos = aPos.value();  // Used to track the position of '@' or '.'. | 
 |   for (size_t i = aPos.value(); i > 0; i--) { | 
 |     wchar_t ch = (*str)[i - 1]; | 
 |     if (ch == L'_' || ch == L'-' || FXSYS_iswalnum(ch)) | 
 |       continue; | 
 |  | 
 |     if (ch != L'.' || i == pPos || i == 1) { | 
 |       if (i == aPos.value()) { | 
 |         // There is '.' or invalid char before '@'. | 
 |         return false; | 
 |       } | 
 |       // End extracting for other invalid chars, '.' at the beginning, or | 
 |       // consecutive '.'. | 
 |       size_t removed_len = i == pPos ? i + 1 : i; | 
 |       *str = str->Last(str->GetLength() - removed_len); | 
 |       break; | 
 |     } | 
 |     // Found a valid '.'. | 
 |     pPos = i - 1; | 
 |   } | 
 |  | 
 |   // Check the domain name part. | 
 |   aPos = str->Find(L'@'); | 
 |   if (!aPos.has_value() || aPos.value() == 0) | 
 |     return false; | 
 |  | 
 |   str->TrimRight(L'.'); | 
 |   // At least one '.' in domain name, but not at the beginning. | 
 |   // TODO(weili): RFC5322 allows domain names to be a local name without '.'. | 
 |   // Check whether we should remove this check. | 
 |   auto ePos = str->Find(L'.', aPos.value() + 1); | 
 |   if (!ePos.has_value() || ePos.value() == aPos.value() + 1) | 
 |     return false; | 
 |  | 
 |   // Validate all other chars in domain name. | 
 |   size_t nLen = str->GetLength(); | 
 |   pPos = 0;  // Used to track the position of '.'. | 
 |   for (size_t i = aPos.value() + 1; i < nLen; i++) { | 
 |     wchar_t wch = (*str)[i]; | 
 |     if (wch == L'-' || FXSYS_iswalnum(wch)) | 
 |       continue; | 
 |  | 
 |     if (wch != L'.' || i == pPos + 1) { | 
 |       // Domain name should end before invalid char. | 
 |       size_t host_end = i == pPos + 1 ? i - 2 : i - 1; | 
 |       if (pPos > 0 && host_end - aPos.value() >= 3) { | 
 |         // Trim the ending invalid chars if there is at least one '.' and name. | 
 |         *str = str->First(host_end + 1); | 
 |         break; | 
 |       } | 
 |       return false; | 
 |     } | 
 |     pPos = i; | 
 |   } | 
 |  | 
 |   if (!str->Contains(L"mailto:")) | 
 |     *str = L"mailto:" + *str; | 
 |  | 
 |   return true; | 
 | } | 
 |  | 
 | WideString CPDF_LinkExtract::GetURL(size_t index) const { | 
 |   return index < m_LinkArray.size() ? m_LinkArray[index].m_strUrl | 
 |                                     : WideString(); | 
 | } | 
 |  | 
 | std::vector<CFX_FloatRect> CPDF_LinkExtract::GetRects(size_t index) const { | 
 |   if (index >= m_LinkArray.size()) | 
 |     return std::vector<CFX_FloatRect>(); | 
 |  | 
 |   return m_pTextPage->GetRectArray(m_LinkArray[index].m_Start, | 
 |                                    m_LinkArray[index].m_Count); | 
 | } | 
 |  | 
 | absl::optional<CPDF_LinkExtract::Range> CPDF_LinkExtract::GetTextRange( | 
 |     size_t index) const { | 
 |   if (index >= m_LinkArray.size()) | 
 |     return absl::nullopt; | 
 |   return m_LinkArray[index]; | 
 | } |