summaryrefslogtreecommitdiff
path: root/Code.php
diff options
context:
space:
mode:
Diffstat (limited to 'Code.php')
-rw-r--r--Code.php234
1 files changed, 234 insertions, 0 deletions
diff --git a/Code.php b/Code.php
new file mode 100644
index 0000000..ca02b3c
--- /dev/null
+++ b/Code.php
@@ -0,0 +1,234 @@
+<?php
+namespace MediaWiki\Extension\Code;
+
+use Content;
+use ErrorPageError;
+use ExtensionRegistry;
+use Html;
+use MediaWiki\Content\Renderer\ContentParseParams;
+use MediaWiki\MediaWikiServices;
+use Parser;
+use ParserOutput;
+use Sanitizer;
+use SpecialPage;
+use TextContent;
+use TextContentHandler;
+use Title;
+use WikiPage;
+use Xml;
+
+class CodeHooks {
+ public static function onParserFirstCallInit( Parser $parser ) {
+ global $wgCode_languages;
+ foreach ( $wgCode_languages as $lang ) {
+ $parser->setHook(
+ $lang['tag'],
+ fn ( $input, $args ) => renderCode( $input, $lang, $parser->getOutput(), $args )
+ );
+ }
+ }
+
+ public static function onContentHandlerDefaultModelFor( Title $title, &$model ) {
+ if ( getLangByPageName( $title->getDBkey() ) ) {
+ $model = CodeContent::MODEL;
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
+
+function getLangByPageName( string $dbKey ) {
+ global $wgCode_languages;
+ $suffix = '.' . pathinfo( $dbKey, PATHINFO_EXTENSION );
+ return current( array_filter( $wgCode_languages, fn( $lang ) => $lang['suffix'] == $suffix ) );
+}
+
+function pre( $code, $attr ) {
+ return Xml::element( 'pre', Sanitizer::validateTagAttributes( $attr, 'pre' ), $code );
+}
+
+/**
+ * Render the given code and return the HTML.
+ *
+ * @param string $code The code to be rendered.
+ * @param ? $lang The language configuration.
+ * @param OutputPage|ParserOutput|null $outputPage
+ * @param ?array $tagAttrs Associative array of tag attributes or null in case of a code page.
+ * @return string The rendered HTML.
+ */
+function renderCode( string $code, $lang, $outputPage, ?array $tagAttrs = null ) {
+ if ( ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' ) ) {
+ $result = \SyntaxHighlight::highlight( $code, $lang['pygmentsLexer'], $tagAttrs ?? [ 'line' => true ] );
+ if ( $result->isOk() ) {
+ $out = $result->getValue();
+ $outputPage->addModuleStyles( [ 'ext.pygments' ] );
+ } else {
+ $out = pre( $code, $tagAttrs );
+ }
+ } else {
+ $out = pre( $code, $tagAttrs );
+ }
+
+ foreach ( $lang['linkifiers'] as $regex => $href ) {
+ $out = preg_replace_callback( $regex, fn( $match ) =>
+ Xml::element( 'a', [
+ 'href' => str_replace( '$1', $match[0], $href ),
+ 'tabindex' => -1
+ ], $match[0] ),
+ $out );
+ }
+
+ foreach ( $lang['actions'] as $action => $url ) {
+ if ( str_contains( $url, '$url' ) ) {
+ // FUTURE: it would be nice to still display such actions for code pages
+ continue;
+ }
+
+ $encodedCode = rawurlencode( trim( $code ) );
+
+ // The wfMessage allows admins to create MediaWiki:codeaction-label with a template call
+ // if they want to enable their users to customize or localize the code action labels.
+ $out .= Xml::openElement( 'a', [ 'href' => str_replace( '$code', $encodedCode, $url ) ] );
+ $out .= wfMessage( "codeaction-label", $action )->parse();
+ $out .= Xml::closeElement( 'a' ) . ' ';
+ }
+
+ return $out;
+}
+
+class ContentHandler extends TextContentHandler {
+ public function __construct( $modelId = CodeContent::MODEL ) {
+ parent::__construct( $modelId, [ CONTENT_FORMAT_TEXT ] );
+ }
+
+ public function fillParserOutput( Content $content, ContentParseParams $params, ParserOutput &$output ) {
+ $lang = getLangByPageName( $params->getPage()->getDBkey() );
+ $out = '';
+ if ( !$lang ) {
+ global $wgCode_languages;
+ $supportedSuffixes = implode( ', ', array_map( fn( $lang ) => $lang['suffix'], $wgCode_languages ) );
+
+ MediaWikiServices::getInstance()->getTrackingCategories()
+ ->addTrackingCategory( $output, 'codepage-error-category', $params->getPage() );
+
+ $out = Html::errorBox( wfMessage( 'codepage-unknown-suffix', 'code', "[$supportedSuffixes]" ) );
+ $lang = [
+ 'pygmentsLexer' => 'text',
+ 'actions' => [],
+ 'linkifiers' => [],
+ ];
+ }
+ $out .= renderCode( $content->getText(), $lang, $output );
+ $output->setText( $out );
+ }
+
+ protected function getContentClass() {
+ return CodeContent::class;
+ }
+}
+
+class CodeContent extends TextContent {
+ // Has to match the ContentHandlers entry in extension.json.
+ public const MODEL = 'code';
+
+ public function __construct( $text, $model_id = self::MODEL ) {
+ parent::__construct( $text, $model_id );
+ }
+}
+
+class SpecialCodeAction extends SpecialPage {
+ public function __construct() {
+ parent::__construct( 'CodeAction' );
+ }
+
+ public function execute( $par ) {
+ $namespacesWithCodePages = $this->getConfig()->get( 'Code_namespacesWithCodePages' );
+ $languages = $this->getConfig()->get( 'Code_languages' );
+
+ if ( empty( $namespacesWithCodePages ) ) {
+ $exampleConfig = htmlspecialchars( file_get_contents( __DIR__ . '/examples/namespacesWithCodePages.php' ) );
+ throw new ErrorPageError( 'codeactions-unavailable-title', 'codeactions-unavailable-no-namespaces', "<pre>$exampleConfig</pre>" );
+ }
+
+ $exampleLang = current( array_filter( $languages, fn( $lang ) => count( $lang['actions'] ) > 0 ) );
+ if ( !$exampleLang ) {
+ $exampleConfig = htmlspecialchars( file_get_contents( __DIR__ . '/examples/languages.php' ) );
+ throw new ErrorPageError( 'codeactions-unavailable-title', 'codeactions-unavailable-no-lang-with-actions', "<pre>$exampleConfig</pre>" );
+ }
+
+ $contLang = MediaWikiServices::getInstance()->getContentLanguage();
+
+ $output = $this->getOutput();
+ $args = explode( '/', $par, 2 );
+
+ if ( count( $args ) != 2 ) {
+ $formatter = MediaWikiServices::getInstance()->getTitleFormatter();
+
+ $title = $formatter->formatTitle( array_keys( $namespacesWithCodePages )[0], 'Example' . $exampleLang['suffix'] );
+ $action = htmlspecialchars( array_keys( $exampleLang['actions'] )[0] );
+ $output->setPageTitle( $this->msg( 'notargettitle' ) );
+ $output->addWikiMsg( 'codeaction-howto', $this->getPageTitle(), $action, $title );
+ $output->addWikiTextAsInterface(
+ Xml::element( 'h2', null, $this->msg( 'codeactions-available' ) )
+ );
+ $output->addHTML(
+ Xml::buildTable(
+ array_map( fn ( $lang ) => [ $lang['suffix'], implode( ', ', array_keys( $lang['actions'] ) ) ], $languages ),
+ [ 'class' => 'wikitable' ], [ $this->msg( 'code-pagesuffix' ), $this->msg( 'codeactions-available' ) ] )
+ );
+ return;
+ }
+ [ $action, $pagename ] = $args;
+
+ $title = Title::newFromText( $pagename );
+ if ( !$title ) {
+ throw new ErrorPageError( 'badtitle', 'title-invalid' );
+ }
+
+ if ( !array_key_exists( $title->getNamespace(), $namespacesWithCodePages ) ) {
+ $nsName = $this->getContext()->getLanguage()->getFormattedNsText( $title->getNamespace() );
+ throw new ErrorPageError( 'codeaction-unsupported-namespace-title', 'codeaction-unsupported-namespace-text', $nsName );
+ }
+
+ $lang = getLangByPageName( $title->getDBkey() );
+ if ( !$lang ) {
+ $supportedSuffixes = implode( ', ', array_map( fn( $lang ) => $lang['suffix'], $languages ) );
+ throw new ErrorPageError( 'codeaction-unknown-suffix-title', 'codeaction-unknown-suffix-text', [ $pagename, "[$supportedSuffixes]" ] );
+ }
+
+ $url = $lang['actions'][$action] ?? null;
+
+ if ( !$url ) {
+ $supportedActions = implode( ', ', array_keys( $lang['actions'] ) );
+ throw new ErrorPageError( 'codeaction-unknown-action-title', 'codeaction-unknown-action-text', [ $action, "[$supportedActions]" ] );
+ }
+
+ if ( str_contains( $url, '$code' ) ) {
+ $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
+ $errors = $permissionManager->getPermissionErrors( 'read', $this->getUser(), $title );
+ if ( !empty( $errors ) ) {
+ $output->showPermissionsErrorPage( $errors, 'read' );
+ return;
+ }
+
+ $content = WikiPage::factory( $title )->getContent();
+ if ( $content == null ) {
+ throw new ErrorPageError( 'codeaction-page-not-found-title', 'codeaction-page-not-found-text', $title );
+ }
+ $content = ContentHandler::getContentText( $content );
+
+ $codePrefix = $lang['codePrefix'] ?? null;
+ if ( $codePrefix ) {
+ $content = str_replace( '$url', $title->getFullURL(), $codePrefix ) . "\n\n" . $content;
+ }
+
+ $url = str_replace( '$code', rawurlencode( $content ), $url );
+ } else {
+ $url = str_replace( '$url', rawurlencode( $title->getFullURL() ), $url );
+ }
+
+ $output->redirect( $url );
+ }
+}