Matrix support

This commit is contained in:
2026-05-31 03:17:16 +09:00
parent 3afeab518f
commit 52d2a46c03
4 changed files with 308 additions and 78 deletions
+7 -4
View File
@@ -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": [],
+20 -20
View File
@@ -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 );
}
+209 -43
View File
@@ -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;
}
}
+72 -11
View File
@@ -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 '<a href="' . self::escapeHtml( self::encodeURL( $url ) ) . '">' .
self::escapeHtml( $text ) . '</a>';
}
/**
@@ -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 '<strong>' . self::escapeHtml( $matches[1] ) . '</strong>';
},
$text
);
$text = preg_replace_callback(
'/`([^`]+)`/',
static function ( array $matches ): string {
return '<code>' . self::escapeHtml( $matches[1] ) . '</code>';
},
$text
);
$text = preg_replace_callback(
'/(?<!\*)\*([^*\n]+)\*(?!\*)/',
static function ( array $matches ): string {
return '<em>' . self::escapeHtml( $matches[1] ) . '</em>';
},
$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+href="([^"]+)">(.+?)<\/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' );
}
}