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' ); + } }