Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/command/Commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,9 @@ define(function (require, exports, module) {
/** Toggles auto update */
exports.HELP_AUTO_UPDATE = "help.autoUpdate"; // shortcuts integrated extension

/** Migrates browser data from the legacy web origin */
exports.HELP_MIGRATE_DATA = "help.migrateData"; // MigrateAssist integrated extension

// Working Set Configuration
/** Sorts working set by order files were added */
exports.CMD_WORKINGSET_SORT_BY_ADDED = "cmd.sortWorkingSetByAdded"; // WorkingSetSort.js _handleSort()
Expand Down
206 changes: 206 additions & 0 deletions src/extensionsIntegrated/MigrateAssist/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

/**
* Shared configuration for the one time move off the legacy web origin onto web.phcode.dev.
*
* Everything that has to change when the rollout moves forward lives here: the two origins and the
* sunset date. `LEGACY_ORIGIN` points at staging while the flow is being validated end to end;
* flipping it to https://phcode.dev is the only edit needed to go live.
*
* @module extensionsIntegrated/MigrateAssist/constants
*/
define(function (require, exports, module) {

// Domain names, package ids and store urls are brand identifiers rather than prose. They must
// render identically in every locale, so they stay here instead of going through strings.js.

/**
* The origin we are migrating away from. Switch to "https://phcode.dev" once the flow has been
* validated against staging.
* @type {string}
*/
const LEGACY_ORIGIN = "https://staging.phcode.dev";

/**
* The origin we are migrating to.
* @type {string}
*/
const NEW_ORIGIN = "https://web.phcode.dev";

/**
* Human readable form of the above, used inside translated sentences via StringUtils.format.
*/
const LEGACY_DOMAIN_NAME = "phcode.dev";
const NEW_DOMAIN_NAME = "web.phcode.dev";

/**
* The day the legacy origin stops serving. Month is 0 based, so 8 is September.
* @type {number}
*/
const SUNSET_DATE = Date.UTC(2026, 8, 1);

/**
* Android/ChromeOS Trusted Web Activity that wraps the legacy origin. Users launched from this
* package need an app update rather than a browser navigation, see sunset-dialog.js.
*/
const TWA_PACKAGE_ID = "prod.phcode.twa";
const TWA_REFERRER_PREFIX = `android-app://${TWA_PACKAGE_ID}`;
const TWA_STORE_URL = `https://play.google.com/store/apps/details?id=${TWA_PACKAGE_ID}`;

/**
* PhStore key recording that the migration already ran. Once set, the automatic path never runs
* again and the user has to ask for it from the Help menu.
* @type {string}
*/
const MIGRATION_DONE_KEY = "migrateAssist.v1.done";

/**
* Dev only override, so the whole cross origin flow can be exercised on one dev server.
* http://localhost:8000 and http://127.0.0.1:8000 are different origins with separate IndexedDB
* but the same files, and both are already trusted, so they make a usable legacy/new pair.
* Set localStorage.MIGRATE_ORIGINS_OVERRIDE to {"legacy": "...", "target": "..."}.
*/
const ORIGINS_OVERRIDE_KEY = "MIGRATE_ORIGINS_OVERRIDE";

let _override = null;
function _getOverride() {
if (_override) {
return _override;
}
_override = {};
// Mirrors the accounts server override in index.html: dev builds only, never in tests, so a
// stray localStorage value can never redirect a production user to an attacker's origin.
if (Phoenix.isTestWindow || !Phoenix.config || Phoenix.config.environment !== "dev") {

Check warning on line 91 in src/extensionsIntegrated/MigrateAssist/constants.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaAq5W-hOVmC7munXgII&open=AaAq5W-hOVmC7munXgII&pullRequest=3130
return _override;
}
try {
const parsed = JSON.parse(localStorage.getItem(ORIGINS_OVERRIDE_KEY));
if (parsed && typeof parsed.legacy === "string" && typeof parsed.target === "string") {
_override = parsed;
console.log("MigrateAssist: using dev origin override", _override);
}
} catch (e) {
console.warn("MigrateAssist: could not read origin override, using defaults", e);
}
return _override;
}

/**
* The origin holding the data to be moved.
* @return {string}
*/
function getLegacyOrigin() {
return _getOverride().legacy || LEGACY_ORIGIN;
}

/**
* The origin the data is being moved into.
* @return {string}
*/
function getNewOrigin() {
return _getOverride().target || NEW_ORIGIN;
}

/**
* True when this window is the site being retired.
* @return {boolean}
*/
function isLegacyOrigin() {
return !Phoenix.isNativeApp && location.origin === getLegacyOrigin();
}

/**
* True when this window is the new home.
* @return {boolean}
*/
function isNewOrigin() {
return !Phoenix.isNativeApp && location.origin === getNewOrigin();
}

/**
* URL of the helper page on the legacy origin.
*
* Both origins serve the same artifact with the same layout, so the helper sits at the same path
* prefix as the page asking for it. In production that is the origin root; on the dev server the
* app lives under /src/, and hardcoding "/" there would 404.
* @return {string}
*/
function getMigrateAssistURL() {
const pathname = location.pathname;
const prefix = pathname.substring(0, pathname.lastIndexOf("/") + 1);
return `${getLegacyOrigin()}${prefix}migrateAssist.html`;
}

/**
* Safari and iOS are deliberately out of scope for the automatic migration. This is a product
* decision rather than a technical limit; the same site iframe would very likely work there too.
* @return {boolean}
*/
function isMigrationSupportedBrowser() {

Check warning on line 157 in src/extensionsIntegrated/MigrateAssist/constants.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function 'isMigrationSupportedBrowser' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaAq5W-hOVmC7munXgIJ&open=AaAq5W-hOVmC7munXgIJ&pullRequest=3130
return !(Phoenix.browser.desktop.isSafari || Phoenix.browser.mobile.isIos);
}

/**
* True when the app was launched from our own Trusted Web Activity. document.referrer only
* reflects the initial navigation, so callers should read this once at boot and cache it.
* @return {boolean}
*/
function isTWALaunch() {
return (document.referrer || "").startsWith(TWA_REFERRER_PREFIX);
}

/**
* Whole days left before the legacy origin is retired, floored at 0.
* @param {number} [now] current time in ms, for tests
* @return {number}
*/
function daysToSunset(now) {
const millisPerDay = 24 * 60 * 60 * 1000;
const remaining = SUNSET_DATE - (typeof now === "number" ? now : Date.now());
if (remaining <= 0) {
return 0;
}
return Math.ceil(remaining / millisPerDay);
}

/**
* @param {number} [now] current time in ms, for tests
* @return {boolean} true once the legacy origin is expected to be gone
*/
function isPastSunset(now) {
return (typeof now === "number" ? now : Date.now()) >= SUNSET_DATE;
}

exports.LEGACY_DOMAIN_NAME = LEGACY_DOMAIN_NAME;
exports.NEW_DOMAIN_NAME = NEW_DOMAIN_NAME;
exports.SUNSET_DATE = SUNSET_DATE;
exports.TWA_STORE_URL = TWA_STORE_URL;
exports.MIGRATION_DONE_KEY = MIGRATION_DONE_KEY;
exports.getLegacyOrigin = getLegacyOrigin;
exports.getMigrateAssistURL = getMigrateAssistURL;
exports.getNewOrigin = getNewOrigin;
exports.isLegacyOrigin = isLegacyOrigin;
exports.isNewOrigin = isNewOrigin;
exports.isMigrationSupportedBrowser = isMigrationSupportedBrowser;
exports.isTWALaunch = isTWALaunch;
exports.daysToSunset = daysToSunset;
exports.isPastSunset = isPastSunset;
});
25 changes: 25 additions & 0 deletions src/extensionsIntegrated/MigrateAssist/html/migrate-progress.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="migrate-assist-dialog template modal">
<div class="modal-header">
<h1 class="dialog-title">{{Strings.MIGRATE_PROGRESS_TITLE}}</h1>
</div>
<div class="modal-body">
<div class="dialog-message">
<p class="migrate-assist-intro">{{introMessage}}</p>
<div class="progress-bar-container">
<div class="progress-bar-background">
<div class="progress progress-bar-foreground migrate-assist-bar" style="width: 0%"></div>
</div>
</div>
<p class="migrate-assist-status">{{Strings.PLEASE_WAIT}}</p>
<p class="migrate-assist-detail forced-hidden"></p>
</div>
</div>
<div class="modal-footer">
<button class="dialog-button btn forced-hidden migrate-assist-close" data-button-id="close">
{{Strings.CLOSE}}
</button>
<button class="dialog-button btn primary forced-hidden migrate-assist-reload" data-button-id="reload">
{{Strings.MIGRATE_RELOAD_NOW}}
</button>
</div>
</div>
76 changes: 76 additions & 0 deletions src/extensionsIntegrated/MigrateAssist/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

/**
* Entry point for the one time move off the legacy web origin onto web.phcode.dev.
*
* Which half runs depends purely on which origin this window is:
* - on the origin being retired, announce the move once per boot (sunset-dialog);
* - on the new origin, quietly check whether anything is left behind and pull it across
* (migrator), plus register the Help menu entry that lets the user ask for it again later.
*
* Anywhere else, including the desktop app, this module does nothing at all.
*
* Styling lives in `../../styles/Extn-MigrateAssist.less`.
*
* @module extensionsIntegrated/MigrateAssist/main
*/
define(function (require, exports, module) {
const AppInit = require("utils/AppInit"),
CommandManager = require("command/CommandManager"),
Commands = require("command/Commands"),
Menus = require("command/Menus"),
Strings = require("strings"),
Constants = require("./constants"),
SunsetDialog = require("./sunset-dialog"),
Migrator = require("./migrator");

function _initLegacyOrigin() {
if (Phoenix.isTestWindow) {
return;
}
SunsetDialog.show();
}

function _initNewOrigin() {
// The menu entry is only registered here, so it can never show up on the legacy origin or on
// desktop. It is also skipped on Safari/iOS, where the migration is deliberately not
// implemented: offering an action we do not honour would be worse than not offering it.
if (!Phoenix.isTestWindow && Constants.isMigrationSupportedBrowser()) {
CommandManager.register(Strings.CMD_MIGRATE_DATA, Commands.HELP_MIGRATE_DATA, function () {
Migrator.runManually();
});
// Anchored to About rather than to Check for Updates: the updater only registers its
// command on the desktop build, so on the web the anchor would not exist and this would
// silently fall through to the very bottom of the menu.
Menus.getMenu(Menus.AppMenuBar.HELP_MENU)
.addMenuItem(Commands.HELP_MIGRATE_DATA, "", Menus.BEFORE, Commands.HELP_ABOUT);
}
Migrator.runOnBoot();
}

AppInit.appReady(function () {
if (Constants.isLegacyOrigin()) {
_initLegacyOrigin();
} else if (Constants.isNewOrigin()) {
_initNewOrigin();
}
});
});
Loading
Loading