diff --git a/src/client.js b/src/client.js
index 43c1b8761..9efdf6d53 100644
--- a/src/client.js
+++ b/src/client.js
@@ -59,6 +59,7 @@ Promise.config({warnings: false});
const SCROLLBACK_DELAY_MS = 3000;
const CRYPTO_ENABLED = isCryptoAvailable();
+const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
const keys = [];
@@ -225,6 +226,8 @@ function MatrixClient(opts) {
this._pushProcessor = new PushProcessor(this);
this._serverSupportsLazyLoading = null;
+
+ this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
}
utils.inherits(MatrixClient, EventEmitter);
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
@@ -392,6 +395,34 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
this._notifTimelineSet = notifTimelineSet;
};
+/**
+ * Gets the capabilities of the homeserver. Always returns an object of
+ * capability keys and their options, which may be empty.
+ * @return {module:client.Promise} Resolves to the capabilities of the homeserver
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixClient.prototype.getCapabilities = function() {
+ if (this._cachedCapabilities) {
+ const now = new Date().getTime();
+ if (now - this._cachedCapabilities.lastUpdated <= CAPABILITIES_CACHE_MS) {
+ return Promise.resolve(this._cachedCapabilities.capabilities);
+ }
+ }
+
+ // We swallow errors because we need a default object anyhow
+ return this._http.authedRequest(
+ undefined, "GET", "/capabilities",
+ ).catch(() => null).then((r) => {
+ if (!r) r = {};
+ const capabilities = r["capabilities"] || {};
+ this._cachedCapabilities = {
+ capabilities: capabilities,
+ lastUpdated: new Date().getTime(),
+ };
+ return capabilities;
+ });
+};
+
// Crypto bits
// ===========
diff --git a/src/models/room.js b/src/models/room.js
index c01027114..a4d5c1874 100644
--- a/src/models/room.js
+++ b/src/models/room.js
@@ -31,7 +31,13 @@ const EventTimelineSet = require("./event-timeline-set");
import ReEmitter from '../ReEmitter';
-const LATEST_ROOM_VERSION = '1';
+// These constants are used as sane defaults when the homeserver doesn't support
+// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
+// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the
+// room versions which are considered okay for people to run without being asked
+// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
+// return an m.room_versions capability.
+const KNOWN_SAFE_ROOM_VERSION = '1';
const SAFE_ROOM_VERSIONS = ['1', '2'];
function synthesizeReceipt(userId, event, receiptType) {
@@ -212,19 +218,76 @@ Room.prototype.getVersion = function() {
* Determines whether this room needs to be upgraded to a new version
* @returns {string?} What version the room should be upgraded to, or null if
* the room does not require upgrading at this time.
+ * @deprecated Use #getRecommendedVersion() instead
*/
Room.prototype.shouldUpgradeToVersion = function() {
- // This almost certainly won't be the way this actually works - this
- // is essentially a stub method.
- // Something like https://github.com/matrix-org/matrix-doc/pull/1804
- // would solve this problem for us.
+ // TODO: Remove this function.
+ // This makes assumptions about which versions are safe, and can easily
+ // be wrong. Instead, people are encouraged to use getRecommendedVersion
+ // which determines a safer value. This function doesn't use that function
+ // because this is not async-capable, and to avoid breaking the contract
+ // we're deprecating this.
+
if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
- return LATEST_ROOM_VERSION;
+ return KNOWN_SAFE_ROOM_VERSION;
}
return null;
};
+/**
+ * Determines the recommended room version for the room. This returns an
+ * object with 3 properties: version as the new version the
+ * room should be upgraded to (may be the same as the current version);
+ * needsUpgrade to indicate if the room actually can be
+ * upgraded (ie: does the current version not match?); and urgent
+ * to indicate if the new version patches a vulnerability in a previous
+ * version.
+ * @returns {Promise<{version: string, needsUpgrade: bool, urgent: bool}>}
+ * Resolves to the version the room should be upgraded to.
+ */
+Room.prototype.getRecommendedVersion = async function() {
+ const capabilities = await this._client.getCapabilities();
+ let versionCap = capabilities["m.room_versions"];
+ if (!versionCap) {
+ versionCap = {
+ default: KNOWN_SAFE_ROOM_VERSION,
+ available: {},
+ };
+ for (const safeVer of SAFE_ROOM_VERSIONS) {
+ versionCap.available[safeVer] = "stable";
+ }
+ }
+
+ const currentVersion = this.getVersion();
+
+ const result = {
+ version: currentVersion,
+ needsUpgrade: false,
+ urgent: false,
+ };
+
+ // If the room is on the default version then nothing needs to change
+ if (currentVersion === versionCap.default) return Promise.resolve(result);
+
+ const stableVersions = Object.keys(versionCap.available)
+ .filter((v) => versionCap.available[v] === 'stable');
+
+ // Check if the room is on an unstable version. We determine urgency based
+ // off the version being in the Matrix spec namespace or not (if the version
+ // is in the current namespace and unstable, the room is probably vulnerable).
+ if (!stableVersions.includes(currentVersion)) {
+ result.version = versionCap.default;
+ result.needsUpgrade = true;
+ result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
+ return Promise.resolve(result);
+ }
+
+ // The room is on a stable, but non-default, version by this point.
+ // No upgrade needed.
+ return Promise.resolve(result);
+};
+
/**
* Determines whether the given user is permitted to perform a room upgrade
* @param {String} userId The ID of the user to test against