viewer.cc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. // Copyright 2014 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/dom_distiller/core/viewer.h"
  5. #include <memory>
  6. #include <string>
  7. #include <vector>
  8. #include "base/json/json_writer.h"
  9. #include "base/metrics/histogram_macros.h"
  10. #include "base/strings/escape.h"
  11. #include "base/strings/strcat.h"
  12. #include "base/strings/string_number_conversions.h"
  13. #include "base/strings/string_util.h"
  14. #include "build/build_config.h"
  15. #include "components/dom_distiller/core/distilled_page_prefs.h"
  16. #include "components/dom_distiller/core/dom_distiller_service.h"
  17. #include "components/dom_distiller/core/experiments.h"
  18. #include "components/dom_distiller/core/proto/distilled_article.pb.h"
  19. #include "components/dom_distiller/core/proto/distilled_page.pb.h"
  20. #include "components/dom_distiller/core/task_tracker.h"
  21. #include "components/dom_distiller/core/url_constants.h"
  22. #include "components/dom_distiller/core/url_utils.h"
  23. #include "components/grit/components_resources.h"
  24. #include "components/strings/grit/components_strings.h"
  25. #include "ui/base/l10n/l10n_util.h"
  26. #include "ui/base/resource/resource_bundle.h"
  27. #include "ui/base/template_expressions.h"
  28. #include "url/gurl.h"
  29. namespace dom_distiller {
  30. namespace viewer {
  31. namespace {
  32. // JS Themes. Must agree with useTheme() in dom_distiller_viewer.js.
  33. const char kDarkJsTheme[] = "dark";
  34. const char kLightJsTheme[] = "light";
  35. const char kSepiaJsTheme[] = "sepia";
  36. // CSS Theme classes. Must agree with classes in distilledpage.css.
  37. const char kDarkCssClass[] = "dark";
  38. const char kLightCssClass[] = "light";
  39. const char kSepiaCssClass[] = "sepia";
  40. // JS FontFamilies. Must agree with useFontFamily() in dom_distiller_viewer.js.
  41. const char kSerifJsFontFamily[] = "serif";
  42. const char kSansSerifJsFontFamily[] = "sans-serif";
  43. const char kMonospaceJsFontFamily[] = "monospace";
  44. // CSS FontFamily classes. Must agree with classes in distilledpage.css.
  45. const char kSerifCssClass[] = "serif";
  46. const char kSansSerifCssClass[] = "sans-serif";
  47. const char kMonospaceCssClass[] = "monospace";
  48. std::string GetPlatformSpecificCss() {
  49. #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
  50. return "";
  51. #else // Desktop
  52. return ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
  53. IDR_DISTILLER_DESKTOP_CSS);
  54. #endif
  55. }
  56. // Maps themes to JS themes.
  57. const std::string GetJsTheme(mojom::Theme theme) {
  58. if (theme == mojom::Theme::kDark)
  59. return kDarkJsTheme;
  60. if (theme == mojom::Theme::kSepia)
  61. return kSepiaJsTheme;
  62. return kLightJsTheme;
  63. }
  64. // Maps themes to CSS classes.
  65. const std::string GetThemeCssClass(mojom::Theme theme) {
  66. if (theme == mojom::Theme::kDark)
  67. return kDarkCssClass;
  68. if (theme == mojom::Theme::kSepia)
  69. return kSepiaCssClass;
  70. return kLightCssClass;
  71. }
  72. // Maps font families to JS font families.
  73. const std::string GetJsFontFamily(mojom::FontFamily font_family) {
  74. if (font_family == mojom::FontFamily::kSerif)
  75. return kSerifJsFontFamily;
  76. if (font_family == mojom::FontFamily::kMonospace)
  77. return kMonospaceJsFontFamily;
  78. return kSansSerifJsFontFamily;
  79. }
  80. // Maps fontFamilies to CSS fontFamily classes.
  81. const std::string GetFontCssClass(mojom::FontFamily font_family) {
  82. if (font_family == mojom::FontFamily::kSerif)
  83. return kSerifCssClass;
  84. if (font_family == mojom::FontFamily::kMonospace)
  85. return kMonospaceCssClass;
  86. return kSansSerifCssClass;
  87. }
  88. void EnsureNonEmptyContent(std::string* content) {
  89. UMA_HISTOGRAM_BOOLEAN("DomDistiller.PageHasDistilledData", !content->empty());
  90. if (content->empty()) {
  91. *content =
  92. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_NO_DATA_CONTENT);
  93. }
  94. }
  95. std::string ReplaceHtmlTemplateValues(const mojom::Theme theme,
  96. const mojom::FontFamily font_family,
  97. const std::string& csp_nonce) {
  98. std::string html_template =
  99. ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
  100. IDR_DOM_DISTILLER_VIEWER_HTML);
  101. // Replace placeholders of the form $i18n{foo} with translated strings
  102. // using ReplaceTemplateExpressions. Do this step first because
  103. // ReplaceStringPlaceholders, below, considers $i18n to be an error.
  104. ui::TemplateReplacements i18n_replacements;
  105. i18n_replacements["title"] =
  106. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_LOADING_TITLE);
  107. i18n_replacements["customizeAppearance"] =
  108. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_CUSTOMIZE_APPEARANCE);
  109. i18n_replacements["fontStyle"] =
  110. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_FONT_STYLE);
  111. i18n_replacements["sansSerifFont"] =
  112. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_SANS_SERIF_FONT);
  113. i18n_replacements["serifFont"] =
  114. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_SERIF_FONT);
  115. i18n_replacements["monospaceFont"] =
  116. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_MONOSPACE_FONT);
  117. i18n_replacements["pageColor"] =
  118. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_PAGE_COLOR);
  119. i18n_replacements["light"] =
  120. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_PAGE_COLOR_LIGHT);
  121. i18n_replacements["sepia"] =
  122. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_PAGE_COLOR_SEPIA);
  123. i18n_replacements["dark"] =
  124. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_PAGE_COLOR_DARK);
  125. i18n_replacements["fontSize"] =
  126. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_FONT_SIZE);
  127. i18n_replacements["small"] =
  128. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_FONT_SIZE_SMALL);
  129. i18n_replacements["large"] =
  130. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_FONT_SIZE_LARGE);
  131. i18n_replacements["close"] =
  132. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_CLOSE);
  133. html_template =
  134. ui::ReplaceTemplateExpressions(html_template, i18n_replacements);
  135. // There shouldn't be any unsubstituted i18n placeholders left.
  136. DCHECK_EQ(html_template.find("$i18n"), std::string::npos);
  137. // Now do other non-i18n string replacements.
  138. std::vector<std::string> substitutions;
  139. std::ostringstream csp;
  140. std::ostringstream css;
  141. std::ostringstream svg;
  142. #if BUILDFLAG(IS_IOS)
  143. // On iOS the content is inlined as there is no API to detect those requests
  144. // and return the local data once a page is loaded.
  145. css << "<style>" << viewer::GetCss() << "</style>";
  146. svg << viewer::GetLoadingImage();
  147. // iOS specific CSP policy to mitigate leaking of data from different
  148. // origins.
  149. csp << "<meta http-equiv=\"Content-Security-Policy\" content=\"";
  150. csp << "default-src 'none'; ";
  151. csp << "script-src 'nonce-" << csp_nonce << "'; ";
  152. // YouTube videos are embedded as an iframe.
  153. csp << "frame-src http://www.youtube.com; ";
  154. csp << "style-src 'unsafe-inline' https://fonts.googleapis.com; ";
  155. // Allows the fallback font-face from the main stylesheet.
  156. csp << "font-src https://fonts.gstatic.com; ";
  157. // Images will be inlined as data-uri if they are valid.
  158. csp << "img-src data:; ";
  159. csp << "form-action 'none'; ";
  160. csp << "base-uri 'none'; ";
  161. csp << "\">";
  162. #else
  163. css << "<link rel=\"stylesheet\" href=\"/" << kViewerCssPath << "\">";
  164. svg << "<img src=\"/" << kViewerLoadingImagePath << "\">";
  165. #endif // BUILDFLAG(IS_IOS)
  166. substitutions.push_back(csp.str()); // $1
  167. substitutions.push_back(css.str()); // $2
  168. substitutions.push_back(GetThemeCssClass(theme) + " " +
  169. GetFontCssClass(font_family)); // $3
  170. substitutions.push_back(l10n_util::GetStringUTF8(
  171. IDS_DOM_DISTILLER_JAVASCRIPT_DISABLED_CONTENT)); // $4
  172. substitutions.push_back(svg.str()); // $5
  173. return base::ReplaceStringPlaceholders(html_template, substitutions, nullptr);
  174. }
  175. } // namespace
  176. const std::string GetUnsafeIncrementalDistilledPageJs(
  177. const DistilledPageProto* page_proto,
  178. bool is_last_page) {
  179. std::string output(page_proto->html());
  180. EnsureNonEmptyContent(&output);
  181. base::Value value(output);
  182. base::JSONWriter::Write(value, &output);
  183. std::string page_update("addToPage(");
  184. page_update += output + ");";
  185. return page_update + GetToggleLoadingIndicatorJs(is_last_page);
  186. }
  187. const std::string GetErrorPageJs() {
  188. std::string title(l10n_util::GetStringUTF8(
  189. IDS_DOM_DISTILLER_VIEWER_FAILED_TO_FIND_ARTICLE_TITLE));
  190. std::string page_update(GetSetTitleJs(title));
  191. base::Value value(l10n_util::GetStringUTF8(
  192. IDS_DOM_DISTILLER_VIEWER_FAILED_TO_FIND_ARTICLE_CONTENT));
  193. std::string output;
  194. base::JSONWriter::Write(value, &output);
  195. page_update += "addToPage(" + output + ");";
  196. page_update += GetSetTextDirectionJs(std::string("auto"));
  197. page_update += GetToggleLoadingIndicatorJs(true);
  198. return page_update;
  199. }
  200. const std::string GetSetTitleJs(std::string title) {
  201. #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
  202. base::Value suffixValue("");
  203. #else // Desktop
  204. std::string suffix(
  205. l10n_util::GetStringUTF8(IDS_DOM_DISTILLER_VIEWER_TITLE_SUFFIX));
  206. base::Value suffixValue(" - " + suffix);
  207. #endif
  208. base::Value titleValue(title);
  209. std::string suffixJs;
  210. base::JSONWriter::Write(suffixValue, &suffixJs);
  211. std::string titleJs;
  212. base::JSONWriter::Write(titleValue, &titleJs);
  213. return "setTitle(" + titleJs + ", " + suffixJs + ");";
  214. }
  215. const std::string GetSetTextDirectionJs(const std::string& direction) {
  216. base::Value value(direction);
  217. std::string output;
  218. base::JSONWriter::Write(value, &output);
  219. return "setTextDirection(" + output + ");";
  220. }
  221. const std::string GetToggleLoadingIndicatorJs(bool is_last_page) {
  222. if (is_last_page)
  223. return "showLoadingIndicator(true);";
  224. return "showLoadingIndicator(false);";
  225. }
  226. const std::string GetArticleTemplateHtml(mojom::Theme theme,
  227. mojom::FontFamily font_family,
  228. const std::string& csp_nonce) {
  229. return ReplaceHtmlTemplateValues(theme, font_family, csp_nonce);
  230. }
  231. const std::string GetUnsafeArticleContentJs(
  232. const DistilledArticleProto* article_proto) {
  233. DCHECK(article_proto);
  234. std::ostringstream unsafe_output_stream;
  235. if (article_proto->pages_size() > 0 && article_proto->pages(0).has_html()) {
  236. for (int page_num = 0; page_num < article_proto->pages_size(); ++page_num) {
  237. unsafe_output_stream << article_proto->pages(page_num).html();
  238. }
  239. }
  240. std::string output(unsafe_output_stream.str());
  241. EnsureNonEmptyContent(&output);
  242. base::JSONWriter::Write(base::Value(output), &output);
  243. std::string page_update("addToPage(");
  244. page_update += output + ");";
  245. return page_update + GetToggleLoadingIndicatorJs(true);
  246. }
  247. const std::string GetCss() {
  248. return base::StrCat(
  249. {ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
  250. IDR_DISTILLER_CSS),
  251. GetPlatformSpecificCss()});
  252. }
  253. const std::string GetLoadingImage() {
  254. return ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
  255. IDR_DISTILLER_LOADING_IMAGE);
  256. }
  257. const std::string GetJavaScript() {
  258. return ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
  259. IDR_DOM_DISTILLER_VIEWER_JS);
  260. }
  261. std::unique_ptr<ViewerHandle> CreateViewRequest(
  262. DomDistillerServiceInterface* dom_distiller_service,
  263. const GURL& url,
  264. ViewRequestDelegate* view_request_delegate,
  265. const gfx::Size& render_view_size) {
  266. if (!url_utils::IsDistilledPage(url)) {
  267. return nullptr;
  268. }
  269. std::string entry_id = url_utils::GetValueForKeyInUrl(url, kEntryIdKey);
  270. bool has_valid_entry_id = !entry_id.empty();
  271. entry_id = base::ToUpperASCII(entry_id);
  272. GURL requested_url(url_utils::GetOriginalUrlFromDistillerUrl(url));
  273. bool has_valid_url = url_utils::IsUrlDistillable(requested_url);
  274. if (has_valid_entry_id && has_valid_url) {
  275. // It is invalid to specify a query param for both |kEntryIdKey| and
  276. // |kUrlKey|.
  277. return nullptr;
  278. }
  279. if (has_valid_entry_id) {
  280. return nullptr;
  281. }
  282. if (has_valid_url) {
  283. return dom_distiller_service->ViewUrl(
  284. view_request_delegate,
  285. dom_distiller_service->CreateDefaultDistillerPage(render_view_size),
  286. requested_url);
  287. }
  288. // It is invalid to not specify a query param for |kEntryIdKey| or |kUrlKey|.
  289. return nullptr;
  290. }
  291. const std::string GetDistilledPageThemeJs(mojom::Theme theme) {
  292. return "useTheme('" + GetJsTheme(theme) + "');";
  293. }
  294. const std::string GetDistilledPageFontFamilyJs(mojom::FontFamily font_family) {
  295. return "useFontFamily('" + GetJsFontFamily(font_family) + "');";
  296. }
  297. const std::string GetDistilledPageFontScalingJs(float scaling) {
  298. return "useFontScaling(" + base::NumberToString(scaling) + ");";
  299. }
  300. } // namespace viewer
  301. } // namespace dom_distiller