Commit b48fafc2 authored by Kathleen Brade's avatar Kathleen Brade Committed by Pier Angelo Vendrame
Browse files

Bug 16940: After update, load local change notes.

Add an about:tbupdate page that displays the first section from
TorBrowser/Docs/ChangeLog.txt and includes a link to the remote
post-update page (typically our blog entry for the release).

Always load about:tbupdate in a content process, but implement the
code that reads the file system (changelog) in the chrome process
for compatibility with future sandboxing efforts.

Also fix bug 29440. Now about:tbupdate is styled as a fairly simple
changelog page that is designed to be displayed via a link that is on
about:tor.
parent 4d11e1b9
Loading
Loading
Loading
Loading
+12 −0
Original line number Original line Diff line number Diff line
// Copyright (c) 2020, The Tor Project, Inc.
// See LICENSE for licensing information.
//
// vim: set sw=2 sts=2 ts=8 et syntax=javascript:

var EXPORTED_SYMBOLS = ["AboutTBUpdateChild"];

const { RemotePageChild } = ChromeUtils.import(
  "resource://gre/actors/RemotePageChild.jsm"
);

class AboutTBUpdateChild extends RemotePageChild {}
+135 −0
Original line number Original line Diff line number Diff line
// Copyright (c) 2020, The Tor Project, Inc.
// See LICENSE for licensing information.
//
// vim: set sw=2 sts=2 ts=8 et syntax=javascript:

"use strict";

var EXPORTED_SYMBOLS = ["AboutTBUpdateParent"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);

const kRequestUpdateMessageName = "FetchUpdateData";

/**
 * This code provides services to the about:tbupdate page. Whenever
 * about:tbupdate needs to do something chrome-privileged, it sends a
 * message that's handled here. It is modeled after Mozilla's about:home
 * implementation.
 */
class AboutTBUpdateParent extends JSWindowActorParent {
  async receiveMessage(aMessage) {
    if (aMessage.name == kRequestUpdateMessageName) {
      return this.getReleaseNoteInfo();
    }
    return undefined;
  }

  get moreInfoURL() {
    try {
      return Services.prefs.getCharPref("torbrowser.post_update.url");
    } catch (e) {}

    // Use the default URL as a fallback.
    return Services.urlFormatter.formatURLPref("startup.homepage_override_url");
  }

  // Read the text from the beginning of the changelog file that is located
  // at TorBrowser/Docs/ChangeLog.txt (or,
  // TorBrowser.app/Contents/Resources/TorBrowser/Docs/ on macOS, to support
  // Gatekeeper signing) and return an object that contains the following
  // properties:
  //   version        e.g., Tor Browser 8.5
  //   releaseDate    e.g., March 31 2019
  //   releaseNotes   details of changes (lines 2 - end of ChangeLog.txt)
  // We attempt to parse the first line of ChangeLog.txt to extract the
  // version and releaseDate. If parsing fails, we return the entire first
  // line in version and omit releaseDate.
  async getReleaseNoteInfo() {
    let info = { moreInfoURL: this.moreInfoURL };

    try {
      // "XREExeF".parent is the directory that contains firefox, i.e.,
      // Browser/ or, TorBrowser.app/Contents/MacOS/ on macOS.
      let f = Services.dirsvc.get("XREExeF", Ci.nsIFile).parent;
      if (AppConstants.platform === "macosx") {
        f = f.parent;
        f.append("Resources");
      }
      f.append("TorBrowser");
      f.append("Docs");
      f.append("ChangeLog.txt");

      // NOTE: We load in the entire file, but only use the first few lines
      // before the first blank line.
      const logLines = (await IOUtils.readUTF8(f.path))
        .replace(/\n\r?\n.*/ms, "")
        .split(/\n\r?/);

      // Read the first line to get the version and date.
      // Assume everything after the last "-" is the date.
      const firstLine = logLines.shift();
      const match = firstLine?.match(/(.*)-+(.*)/);
      if (match) {
        info.version = match[1].trim();
        info.releaseDate = match[2].trim();
      } else {
        // No date.
        info.version = firstLine?.trim();
      }

      // We want to read the rest of the release notes as a tree. Each entry
      // will contain the text for that line.
      // We choose a negative index for the top node of this tree to ensure no
      // line will appear less indented.
      const topEntry = { indent: -1, children: undefined };
      let prevEntry = topEntry;

      for (let line of logLines) {
        const indent = line.match(/^ */)[0];
        line = line.trim();
        if (line.startsWith("*")) {
          // Treat as a bullet point.
          let entry = {
            text: line.replace(/^\*\s/, ""),
            indent: indent.length,
          };
          let parentEntry;
          if (entry.indent > prevEntry.indent) {
            // A sub-list of the previous item.
            prevEntry.children = [];
            parentEntry = prevEntry;
          } else {
            // Same list or end of sub-list.
            // Search for the first parent whose indent comes before ours.
            parentEntry = prevEntry.parent;
            while (entry.indent <= parentEntry.indent) {
              parentEntry = parentEntry.parent;
            }
          }
          entry.parent = parentEntry;
          parentEntry.children.push(entry);
          prevEntry = entry;
        } else if (prevEntry === topEntry) {
          // Unexpected, missing bullet point on first line.
          // Place as its own bullet point instead, and set as prevEntry for the
          // next loop.
          prevEntry = { text: line, indent: indent.length, parent: topEntry };
          topEntry.children = [prevEntry];
        } else {
          // Append to the previous bullet point.
          prevEntry.text += ` ${line}`;
        }
      }

      info.releaseNotes = topEntry.children;
    } catch (e) {
      console.error(e);
    }

    return info;
  }
}
+6 −0
Original line number Original line Diff line number Diff line
@@ -90,3 +90,9 @@ FINAL_TARGET_FILES.actors += [
BROWSER_CHROME_MANIFESTS += [
BROWSER_CHROME_MANIFESTS += [
    "test/browser/browser.ini",
    "test/browser/browser.ini",
]
]

if CONFIG["BASE_BROWSER_UPDATE"]:
    FINAL_TARGET_FILES.actors += [
        "AboutTBUpdateChild.jsm",
        "AboutTBUpdateParent.jsm",
    ]
+67 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (c) 2019, The Tor Project, Inc.
 * See LICENSE for licensing information.
 *
 * vim: set sw=2 sts=2 ts=8 et syntax=css:
 */

:root {
  --abouttor-text-color: white;
  --abouttor-bg-toron-color: #420C5D;
}

body {
  font-family: Helvetica, Arial, sans-serif;
  color: var(--abouttor-text-color);
  background-color: var(--abouttor-bg-toron-color);
  margin-block: 40px;
  margin-inline: 50px;
  display: grid;
  grid-template-columns: auto auto;
  align-items: baseline;
  gap: 40px 50px;
}

body > *:not([hidden]) {
  display: contents;
}

.label-column {
  grid-column: 1;
}

.content {
  grid-column: 2;
  font-family: monospace;
  line-height: 1.4;
}

.label-column, .content {
  margin: 0;
  padding: 0;
  font-size: 1rem;
  font-weight: normal;
}

a {
  color: inherit;
}

.no-line-break {
  white-space: nowrap;
}

ul {
  padding-inline: 1em 0;
}

h3, h4 {
  font-size: 1.1rem;
  font-weight: bold;
}

h3.build-system-heading {
  font-size: 1.5rem;
  font-weight: normal;
  margin-block-start: 3em;
}
+109 −0
Original line number Original line Diff line number Diff line
// Copyright (c) 2020, The Tor Project, Inc.
// See LICENSE for licensing information.
//
// vim: set sw=2 sts=2 ts=8 et syntax=javascript:

/* eslint-env mozilla/remote-page */

/**
 * An object representing a bullet point in the release notes.
 *
 * typedef {Object} ReleaseBullet
 * @property {string} text - The text for this bullet point.
 * @property {?Array<ReleaseBullet>} children - A sub-list of bullet points.
 */

/**
 * Fill an element with the given list of release bullet points.
 *
 * @param {Element} container - The element to fill with bullet points.
 * @param {Array<ReleaseBullet>} bulletPoints - The list of bullet points.
 * @param {string} [childTag="h3"] - The element tag name to use for direct
 *   children. Initially, the children are h3 sub-headings.
 */
function fillReleaseNotes(container, bulletPoints, childTag = "h3") {
  for (const { text, children } of bulletPoints) {
    const childEl = document.createElement(childTag);
    // Keep dashes like "[tor-browser]" on the same line by nowrapping the word.
    for (const [index, part] of text.split(/(\S+-\S+)/).entries()) {
      if (!part) {
        continue;
      }
      const span = document.createElement("span");
      span.textContent = part;
      span.classList.toggle("no-line-break", index % 2);
      childEl.appendChild(span);
    }
    container.appendChild(childEl);
    if (children) {
      if (childTag == "h3" && text.toLowerCase() === "build system") {
        // Special case: treat the "Build System" heading's children as
        // sub-headings.
        childEl.classList.add("build-system-heading");
        fillReleaseNotes(container, children, "h4");
      } else {
        const listEl = document.createElement("ul");
        fillReleaseNotes(listEl, children, "li");
        if (childTag == "li") {
          // Insert within the "li" element.
          childEl.appendChild(listEl);
        } else {
          container.appendChild(listEl);
        }
      }
    }
  }
}

/**
 * Set the content for the specified container, or hide it if we have no
 * content.
 *
 * @template C
 * @param {string} containerId - The id for the container.
 * @param {?C} content - The content for this container, or a falsey value if
 *   the container has no content.
 * @param {function(contentEl: Elemenet, content: C)} [fillContent] - A function
 *   to fill the ".content" contentEl with the given 'content'. If unspecified,
 *   the 'content' will become the contentEl's textContent.
 */
function setContent(containerId, content, fillContent) {
  const container = document.getElementById(containerId);
  if (!content) {
    container.hidden = true;
    return;
  }
  const contentEl = container.querySelector(".content");
  // Release notes are only in English.
  contentEl.setAttribute("lang", "en-US");
  contentEl.setAttribute("dir", "ltr");
  if (fillContent) {
    fillContent(contentEl, content);
  } else {
    contentEl.textContent = content;
  }
}

/**
 * Callback when we receive the update details.
 *
 * @param {Object} aData - The update details.
 * @param {?string} aData.version - The update version.
 * @param {?string} aData.releaseDate - The release date.
 * @param {?string} aData.moreInfoURL - A URL for more info.
 * @param {?Array<ReleaseBullet>} aData.releaseNotes - Release notes as bullet
 *   points.
 */
function onUpdate(aData) {
  setContent("version-row", aData.version);
  setContent("releasedate-row", aData.releaseDate);
  setContent("releasenotes", aData.releaseNotes, fillReleaseNotes);

  if (aData.moreInfoURL) {
    document.getElementById("infolink").setAttribute("href", aData.moreInfoURL);
  } else {
    document.getElementById("fullinfo").hidden = true;
  }
}

RPMSendQuery("FetchUpdateData").then(onUpdate);
Loading