diff --git a/extension.json b/extension.json
index 09bae5c..4058831 100644
--- a/extension.json
+++ b/extension.json
@@ -1,9 +1,10 @@
{
"name": "Matrix",
"author": [
- "[https://github.com/jayktaylor Jayden Bailey]"
+ "[https://github.com/jayktaylor Jayden Bailey]",
+ "[https://git.pvv.ntnu.no/Projects Programvareverkstedet]"
],
- "url": "https://github.com/jayktaylor/mw-matrix",
+ "url": "https://git.pvv.ntnu.no/Projects/mediawiki-matrix-notifs",
"version": "1.2",
"descriptionmsg": "matrix-desc",
"license-name": "MIT",
@@ -12,11 +13,13 @@
},
"manifest_version": 1,
"config": {
- "MatrixWebhookURL": [],
+ "MatrixHomeserverURL": "",
+ "MatrixUser": "",
+ "MatrixPassword": "",
+ "MatrixRooms": [],
"MatrixNoBots": true,
"MatrixNoMinor": false,
"MatrixNoNull": true,
- "MatrixSuppressPreviews": true,
"MatrixMaxChars": null,
"MatrixMaxCharsUsernames": 25,
"MatrixDisabledHooks": [],
diff --git a/src/MatrixHooks.php b/src/MatrixHooks.php
index 10fb384..7b55476 100644
--- a/src/MatrixHooks.php
+++ b/src/MatrixHooks.php
@@ -111,7 +111,7 @@ class MatrixHooks implements
}
$msg = wfMessage( $msgKey, MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $wikiPage->getTitle(),
+ MatrixUtils::createLink( $wikiPage->getTitle(),
$wikiPage->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ),
MatrixUtils::createRevisionText( $revisionRecord ),
( $summary ? ( '`' . MatrixUtils::sanitiseText(
@@ -145,7 +145,7 @@ class MatrixHooks implements
}
$msg = wfMessage( 'matrix-msg-page-deleted', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $page->getTitle(),
+ MatrixUtils::createLink( $page->getTitle(),
$page->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ),
( $reason ? ( '`' . MatrixUtils::sanitiseText( MatrixUtils::truncateText( $reason ) ) . '`' ) : '' ),
$archivedRevisionCount )->inContentLanguage()->plain();
@@ -181,7 +181,7 @@ class MatrixHooks implements
$msg = wfMessage( 'matrix-msg-page-undeleted', MatrixUtils::createUserLinks( $user ),
( $created ? '' : wfMessage( 'matrix-msg-page-undeleted-revs' )->inContentLanguage()->text() ),
- MatrixUtils::createMarkdownLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
+ MatrixUtils::createLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
( $reason ? ( '`' . MatrixUtils::sanitiseText(
MatrixUtils::truncateText( $reason ) ) . '`' ) : '' ) )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
@@ -210,7 +210,7 @@ class MatrixHooks implements
$msg = wfMessage( 'matrix-msg-rev-visibility-changed', MatrixUtils::createUserLinks( $user ),
count( $visibilityChangeMap ),
- MatrixUtils::createMarkdownLink( $title,
+ MatrixUtils::createLink( $title,
$title->getFullURL( '', false, PROTO_CANONICAL ) ) )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
return true;
@@ -237,7 +237,7 @@ class MatrixHooks implements
}
$msg = wfMessage( 'matrix-msg-page-protect', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $wikiPage->getTitle(),
+ MatrixUtils::createLink( $wikiPage->getTitle(),
$wikiPage->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ),
( $reason ? ( '`' . MatrixUtils::sanitiseText( MatrixUtils::truncateText( $reason ) ) . '`' ) : '' ),
implode( ", ", $protect ) )->inContentLanguage()->plain();
@@ -278,9 +278,9 @@ class MatrixHooks implements
}
$msg = wfMessage( 'matrix-msg-page-moved', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $old,
+ MatrixUtils::createLink( $old,
Title::castFromLinkTarget( $old )->getFullURL( '', false, PROTO_CANONICAL ) ),
- MatrixUtils::createMarkdownLink( $new,
+ MatrixUtils::createLink( $new,
Title::castFromLinkTarget( $new )->getFullURL( '', false, PROTO_CANONICAL ) ),
( $reason ? ( '`' . MatrixUtils::sanitiseText( MatrixUtils::truncateText( $reason ) ) . '`' ) : '' ),
MatrixUtils::createRevisionText( $revision ) )->inContentLanguage()->plain();
@@ -427,7 +427,7 @@ class MatrixHooks implements
$msg = wfMessage( 'matrix-msg-file-upload', MatrixUtils::createUserLinks( $user ),
( $isNewRevision ? wfMessage( 'matrix-msg-file-upload-new' )->inContentLanguage()->text() : '' ),
- MatrixUtils::createMarkdownLink( $lf->getName(),
+ MatrixUtils::createLink( $lf->getName(),
$lf->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ),
( $comment ? ( '`' . MatrixUtils::sanitiseText( MatrixUtils::truncateText( $comment ) ) . '`' ) : '' ),
MatrixUtils::formatBytes( $lf->getSize() ),
@@ -465,7 +465,7 @@ class MatrixHooks implements
}
$msg = wfMessage( 'matrix-msg-file-delete', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $file->getName(),
+ MatrixUtils::createLink( $file->getName(),
$file->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ),
( $reason ? ( '`' . MatrixUtils::sanitiseText(
MatrixUtils::truncateText( $reason ) ) . '`' ) : '' ) )->inContentLanguage()->plain();
@@ -494,7 +494,7 @@ class MatrixHooks implements
}
$msg = wfMessage( 'matrix-msg-file-undelete', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
+ MatrixUtils::createLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
( $reason ? ( '`' . MatrixUtils::sanitiseText(
MatrixUtils::truncateText( $reason ) ) . '`' ) : '' ) )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
@@ -525,7 +525,7 @@ class MatrixHooks implements
}
$msg = wfMessage( 'matrix-msg-import', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
+ MatrixUtils::createLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
$revCount, $sRevCount )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
return true;
@@ -552,8 +552,8 @@ class MatrixHooks implements
}
$msg = wfMessage( 'matrix-msg-page-merge', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $targetTitle, $targetTitle->getFullURL( '', false, PROTO_CANONICAL ) ),
- MatrixUtils::createMarkdownLink( $destTitle,
+ MatrixUtils::createLink( $targetTitle, $targetTitle->getFullURL( '', false, PROTO_CANONICAL ) ),
+ MatrixUtils::createLink( $destTitle,
$destTitle->getFullURL( '', false, PROTO_CANONICAL ) ) )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
return true;
@@ -588,8 +588,8 @@ class MatrixHooks implements
$revAuthor = MatrixUtils::createUserLinks( $rev->getUser( RevisionRecord::RAW ) );
$msg = wfMessage( 'matrix-msg-ext-approvedrevs-approved', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
- MatrixUtils::createMarkdownLink( $rev_id, $revLink ),
+ MatrixUtils::createLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
+ MatrixUtils::createLink( $rev_id, $revLink ),
$revAuthor )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
return true;
@@ -618,7 +618,7 @@ class MatrixHooks implements
}
$msg = wfMessage( 'matrix-msg-ext-approvedrevs-unapproved', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $title,
+ MatrixUtils::createLink( $title,
$title->getFullURL( '', false, PROTO_CANONICAL ) ) )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
return true;
@@ -655,8 +655,8 @@ class MatrixHooks implements
$displayedFile->getUploader() );
$msg = wfMessage( 'matrix-msg-ext-approvedrevs-approved-file', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
- MatrixUtils::createMarkdownLink( 'direct', $displayedFileUrl ),
+ MatrixUtils::createLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
+ MatrixUtils::createLink( 'direct', $displayedFileUrl ),
MatrixUtils::createUserLinks( $uploader ) )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
return true;
@@ -684,7 +684,7 @@ class MatrixHooks implements
}
$msg = wfMessage( 'matrix-msg-ext-approvedrevs-unapproved-file', MatrixUtils::createUserLinks( $user ),
- MatrixUtils::createMarkdownLink( $title,
+ MatrixUtils::createLink( $title,
$title->getFullURL( '', false, PROTO_CANONICAL ) ) )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
return true;
@@ -709,7 +709,7 @@ class MatrixHooks implements
$msg = wfMessage( 'matrix-msg-user-rename', MatrixUtils::createUserLinks( $user ),
"*$old*",
- MatrixUtils::createMarkdownLink( $new,
+ MatrixUtils::createLink( $new,
$renamedUserAsTitle->getFullURL( '', false, PROTO_CANONICAL ) ) )->inContentLanguage()->plain();
$this->messageSender->sendToMatrix( $hookName, $msg );
}
diff --git a/src/MatrixMessageSender.php b/src/MatrixMessageSender.php
index 7d06103..6798714 100644
--- a/src/MatrixMessageSender.php
+++ b/src/MatrixMessageSender.php
@@ -8,7 +8,9 @@ use MediaWiki\Http\HttpRequestFactory;
use Wikimedia\Http\MultiHttpClient;
class MatrixMessageSender {
- public MultiHttpClient $multiHttpClient;
+ private MultiHttpClient $multiHttpClient;
+ private ?string $accessToken = null;
+ private ?string $accessTokenConfigKey = null;
public function __construct( HttpRequestFactory $httpRequestFactory ) {
$this->multiHttpClient = $httpRequestFactory->createMultiClient( [
@@ -24,71 +26,235 @@ class MatrixMessageSender {
* @return void
*/
public function sendToMatrix( string $hookName, string $msg ) {
- global $wgMatrixWebhookURL, $wgMatrixEmojis, $wgMatrixUseEmojis, $wgMatrixPrependTimestamp;
+ global $wgMatrixEmojis, $wgMatrixUseEmojis, $wgMatrixPrependTimestamp;
- if ( !$wgMatrixWebhookURL ) {
- return;
- }
-
- $urls = [];
-
- if ( is_array( $wgMatrixWebhookURL ) ) {
- $urls = array_merge( $urls, $wgMatrixWebhookURL );
- } elseif ( is_string( $wgMatrixWebhookURL ) ) {
- $urls[] = $wgMatrixWebhookURL;
- } else {
- wfDebugLog( 'matrix',
- 'The value of $wgMatrixWebhookURL is not valid and therefore no webhooks could be sent.' );
+ $config = $this->getConnectionConfig();
+ if ( $config === null ) {
return;
}
// Strip whitespace to just one space
- $stripped = preg_replace( '/\s+/', ' ', $msg );
+ $stripped = preg_replace( '/\s+/', ' ', $msg ) ?? $msg;
if ( $wgMatrixPrependTimestamp ) {
- $dateString = gmdate( wfMessage( 'matrix-timestampformat' )->inContentLanguage()->text() );
- $stripped = $dateString . ' ' . $stripped;
+ $dateFormat = str_replace(
+ '**',
+ '',
+ wfMessage( 'matrix-timestampformat' )->inContentLanguage()->text()
+ );
+ $dateString = gmdate( trim( $dateFormat ) );
+ $stripped = trim( $dateString . ' ' . $stripped );
}
- if ( $wgMatrixUseEmojis ) {
- $emoji = $wgMatrixEmojis[$hookName];
- $stripped = $emoji . ' ' . $stripped;
+ if ( $wgMatrixUseEmojis && isset( $wgMatrixEmojis[$hookName] ) ) {
+ $stripped = $wgMatrixEmojis[$hookName] . ' ' . $stripped;
}
- $reqs = [];
- foreach ( $urls as $url ) {
- $reqs[] = $this->buildRequest( $url, $stripped );
- }
+ DeferredUpdates::addCallableUpdate( function () use ( $config, $stripped ) {
+ $accessToken = $this->getAccessToken(
+ $config['homeserverUrl'],
+ $config['user'],
+ $config['password']
+ );
+
+ if ( $accessToken === null ) {
+ return;
+ }
+
+ $payload = [
+ 'msgtype' => 'm.text',
+ 'body' => MatrixUtils::toPlainText( $stripped ),
+ 'format' => 'org.matrix.custom.html',
+ 'formatted_body' => MatrixUtils::toMatrixHtml( $stripped ),
+ ];
+
+ $reqs = [];
+ foreach ( $config['rooms'] as $room ) {
+ $reqs[] = $this->buildMessageRequest(
+ $config['homeserverUrl'],
+ $room,
+ $accessToken,
+ $payload
+ );
+ }
- DeferredUpdates::addCallableUpdate( function () use ( $reqs ) {
try {
- $this->multiHttpClient->runMulti( $reqs );
- } catch ( Exception ) {
- // TODO: better logging
+ $responses = $this->multiHttpClient->runMulti( $reqs );
+
+ foreach ( $responses as $req ) {
+ $response = $req['response'] ?? null;
+ if ( !is_array( $response ) ) {
+ wfDebugLog( 'matrix', 'Matrix send request returned no response.' );
+ continue;
+ }
+
+ $this->logIfRequestFailed(
+ 'Failed to send Matrix message to room "' . ( $req['matrixRoom'] ?? 'unknown' ) . '"',
+ $response
+ );
+ }
+ } catch ( Exception $e ) {
+ wfDebugLog( 'matrix', 'Failed to send Matrix message: ' . $e->getMessage() );
}
} );
}
/**
- * Builds a request for use with MultiHttpClient
- * @param string $url
- * @param string $msg
- * @return array
+ * @return array{homeserverUrl:string,user:string,password:string,rooms:string[]}|null
*/
- private function buildRequest( string $url, string $msg ): array {
+ private function getConnectionConfig(): ?array {
+ global $wgMatrixHomeserverURL, $wgMatrixUser, $wgMatrixPassword, $wgMatrixRooms;
+
+ if ( !is_string( $wgMatrixHomeserverURL ) || trim( $wgMatrixHomeserverURL ) === '' ) {
+ wfDebugLog( 'matrix',
+ 'The value of $wgMatrixHomeserverURL is not valid and therefore no messages could be sent.' );
+ return null;
+ }
+
+ if ( !is_string( $wgMatrixUser ) || trim( $wgMatrixUser ) === '' ) {
+ wfDebugLog( 'matrix',
+ 'The value of $wgMatrixUser is not valid and therefore no messages could be sent.' );
+ return null;
+ }
+
+ if ( !is_string( $wgMatrixPassword ) || trim( $wgMatrixPassword ) === '' ) {
+ wfDebugLog( 'matrix',
+ 'The value of $wgMatrixPassword is not valid and therefore no messages could be sent.' );
+ return null;
+ }
+
+ $rooms = [];
+ if ( is_array( $wgMatrixRooms ) ) {
+ foreach ( $wgMatrixRooms as $room ) {
+ if ( is_string( $room ) && trim( $room ) !== '' ) {
+ $rooms[] = trim( $room );
+ }
+ }
+ } elseif ( is_string( $wgMatrixRooms ) && trim( $wgMatrixRooms ) !== '' ) {
+ $rooms[] = trim( $wgMatrixRooms );
+ } else {
+ wfDebugLog( 'matrix',
+ 'The value of $wgMatrixRooms is not valid and therefore no messages could be sent.' );
+ return null;
+ }
+
+ $rooms = array_values( array_unique( $rooms ) );
+ if ( $rooms === [] ) {
+ wfDebugLog( 'matrix',
+ 'The value of $wgMatrixRooms is empty and therefore no messages could be sent.' );
+ return null;
+ }
+
+ return [
+ 'homeserverUrl' => rtrim( trim( $wgMatrixHomeserverURL ), '/' ),
+ 'user' => trim( $wgMatrixUser ),
+ 'password' => $wgMatrixPassword,
+ 'rooms' => $rooms,
+ ];
+ }
+
+ private function getAccessToken( string $homeserverUrl, string $user, string $password ): ?string {
+ $configKey = md5( $homeserverUrl . "\n" . $user . "\n" . $password );
+
+ if ( $this->accessToken !== null && $this->accessTokenConfigKey === $configKey ) {
+ return $this->accessToken;
+ }
+
+ try {
+ $response = $this->multiHttpClient->run(
+ $this->buildLoginRequest( $homeserverUrl, $user, $password )
+ );
+ } catch ( Exception $e ) {
+ wfDebugLog( 'matrix', 'Failed to log in to Matrix: ' . $e->getMessage() );
+ return null;
+ }
+
+ if ( $this->logIfRequestFailed( 'Failed to log in to Matrix', $response ) ) {
+ return null;
+ }
+
+ $responseBody = $response['body'] ?? '';
+ $data = json_decode( $responseBody, true );
+
+ if ( !is_array( $data ) || !isset( $data['access_token'] ) || !is_string( $data['access_token'] ) ) {
+ wfDebugLog( 'matrix',
+ 'Matrix login did not return a valid access_token. Response body: ' . $responseBody );
+ return null;
+ }
+
+ $this->accessToken = $data['access_token'];
+ $this->accessTokenConfigKey = $configKey;
+
+ return $this->accessToken;
+ }
+
+ private function buildLoginRequest( string $homeserverUrl, string $user, string $password ): array {
return [
'method' => 'POST',
- 'url' => $url,
- 'headers' => [
- 'Content-Type' => 'application/json',
- 'User-Agent' => 'mw-matrix/1.2 (github.com/jayktaylor)'
- ],
+ 'url' => $homeserverUrl . '/_matrix/client/r0/login',
+ 'headers' => $this->getJsonHeaders(),
'body' => json_encode( [
- 'content' => $msg,
- 'allowed_mentions' => [
- 'parse' => []
- ]
+ 'type' => 'm.login.password',
+ 'user' => $user,
+ 'password' => $password,
] )
];
}
+
+ private function buildMessageRequest(
+ string $homeserverUrl,
+ string $room,
+ string $accessToken,
+ array $payload
+ ): array {
+ return [
+ 'method' => 'PUT',
+ 'url' => $homeserverUrl . '/_matrix/client/r0/rooms/' .
+ rawurlencode( $room ) . '/send/m.room.message/' . rawurlencode( $this->generateTxnId() ),
+ 'query' => [
+ 'access_token' => $accessToken,
+ ],
+ 'headers' => $this->getJsonHeaders(),
+ 'body' => json_encode( $payload ),
+ 'matrixRoom' => $room,
+ ];
+ }
+
+ private function getJsonHeaders(): array {
+ return [
+ 'Content-Type' => 'application/json',
+ 'User-Agent' => 'mw-matrix (git.pvv.ntnu.no/Projects/mediawiki-matrix-notifs)',
+ ];
+ }
+
+ private function generateTxnId(): string {
+ return str_replace( '.', '-', uniqid( 'mw-matrix-', true ) );
+ }
+
+ /**
+ * @param string $context
+ * @param array $response
+ * @return bool True when the request failed
+ */
+ private function logIfRequestFailed( string $context, array $response ): bool {
+ $code = (int)( $response['code'] ?? 0 );
+
+ if ( $code >= 200 && $code < 300 ) {
+ return false;
+ }
+
+ $error = $response['error'] ?? '';
+ $reason = $response['reason'] ?? '';
+ $body = $response['body'] ?? '';
+
+ wfDebugLog(
+ 'matrix',
+ $context . ': HTTP ' . $code .
+ ( $reason !== '' ? ' ' . $reason : '' ) .
+ ( $error !== '' ? ' (' . $error . ')' : '' ) .
+ ( $body !== '' ? ' ' . $body : '' )
+ );
+
+ return true;
+ }
}
diff --git a/src/MatrixUtils.php b/src/MatrixUtils.php
index 824559d..febb82e 100644
--- a/src/MatrixUtils.php
+++ b/src/MatrixUtils.php
@@ -64,16 +64,14 @@ class MatrixUtils {
}
/**
- * Creates a formatted markdown link based on text and given URL
+ * Creates a formatted link.
* @param string $text
* @param string $url
* @return string
*/
- public static function createMarkdownLink( string $text, string $url ): string {
- global $wgMatrixSuppressPreviews;
-
- return "[" . $text . "]" . '(' . ( $wgMatrixSuppressPreviews ? '<' : '' ) .
- self::encodeURL( $url ) . ( $wgMatrixSuppressPreviews ? '>' : '' ) . ')';
+ public static function createLink( string $text, string $url ): string {
+ return '' .
+ self::escapeHtml( $text ) . '';
}
/**
@@ -101,16 +99,17 @@ class MatrixUtils {
}
}
- $userPage = self::createMarkdownLink( $user_abbr, ( $isAnon ? $contribs : $user->getUserPage() )
+ $userPage = self::createLink( $user_abbr, ( $isAnon ? $contribs : $user->getUserPage() )
->getFullURL( '', false, PROTO_CANONICAL ) );
- $userTalk = self::createMarkdownLink( wfMessage( 'matrix-talk' )->inContentLanguage()->text(),
+ $userTalk = self::createLink( wfMessage( 'matrix-talk' )->inContentLanguage()->text(),
$user->getTalkPage()->getFullURL( '', false, PROTO_CANONICAL ) );
- $userContribs = self::createMarkdownLink( wfMessage( 'matrix-contribs' )->inContentLanguage()->text(),
+ $userContribs = self::createLink( wfMessage( 'matrix-contribs' )->inContentLanguage()->text(),
$contribs->getFullURL( '', false, PROTO_CANONICAL ) );
$text = wfMessage( 'matrix-userlinks', $userPage, $userTalk, $userContribs )->inContentLanguage()->text();
} else {
// If we were given a string, handle this differently.
- $text = wfMessage( 'matrix-userlinks', $user, 'n/a', 'n/a' )->inContentLanguage()->text();
+ $text = wfMessage( 'matrix-userlinks', self::escapeHtml( (string)$user ), 'n/a', 'n/a' )
+ ->inContentLanguage()->text();
}
return $text;
}
@@ -121,7 +120,7 @@ class MatrixUtils {
* @return string
*/
public static function createRevisionText( RevisionRecord $revision ): string {
- $diff = self::createMarkdownLink( wfMessage( 'matrix-diff' )->inContentLanguage()->text(),
+ $diff = self::createLink( wfMessage( 'matrix-diff' )->inContentLanguage()->text(),
Title::newFromLinkTarget( $revision->getPageAsLinkTarget() )->getFullURL(
[ 'diff' => 'prev', 'oldid' => $revision->getId() ], false, PROTO_CANONICAL ) );
$minor = '';
@@ -143,6 +142,60 @@ class MatrixUtils {
return wfMessage( 'matrix-revisionlinks', $diff, $minor, $size )->inContentLanguage()->text();
}
+ /**
+ * Converts extension-generated formatting to Matrix HTML.
+ * @param string $text
+ * @return string
+ */
+ public static function toMatrixHtml( string $text ): string {
+ $text = preg_replace_callback(
+ '/\*\*(.+?)\*\*/s',
+ static function ( array $matches ): string {
+ return '' . self::escapeHtml( $matches[1] ) . '';
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ '/`([^`]+)`/',
+ static function ( array $matches ): string {
+ return '' . self::escapeHtml( $matches[1] ) . '';
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ '/(?' . self::escapeHtml( $matches[1] ) . '';
+ },
+ $text
+ );
+
+ return $text;
+ }
+
+ /**
+ * Converts extension-generated formatting to plain text.
+ * @param string $text
+ * @return string
+ */
+ public static function toPlainText( string $text ): string {
+ $html = self::toMatrixHtml( $text );
+ $html = preg_replace_callback(
+ '/(.+?)<\/a>/s',
+ static function ( array $matches ): string {
+ $label = html_entity_decode( strip_tags( $matches[2] ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
+ $url = html_entity_decode( $matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8' );
+ return $label . ': ' . $url;
+ },
+ $html
+ ) ?? $html;
+
+ $plain = html_entity_decode( strip_tags( $html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
+ return trim( preg_replace( '/\s+/', ' ', $plain ) ?? $plain );
+ }
+
/**
* Strip bad characters from a URL
* @param string $url
@@ -196,4 +249,12 @@ class MatrixUtils {
public static function sanitiseText( string $text ): string {
return preg_replace( '/([`@])/', '', $text );
}
+
+ /**
+ * @param string $text
+ * @return string
+ */
+ private static function escapeHtml( string $text ): string {
+ return htmlspecialchars( $text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
+ }
}