Toggle menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

ProleWiki:Tasks/Technical: Difference between revisions

From ProleWiki, the proletarian encyclopedia
More languages
m (Next function in the backtrace)
Tag: Visual edit
(Added changes done to current extensions)
Tag: Visual edit
 
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
=== Module pages in English and Portuguese instances produces errors ===
== Notification system for ConfirmAccount extension ==
The following is to detail how the Discord extension works, in hopes to implement that on the ConfirmAccount extension.
 
=== Discord extension as reference for Discord notification ===
There are two main files (Discord.php and DiscordUtils.php) that serve the Discord extension, and together they compose the logic of the notification system.
 
==== Discord.php ====
This example function uses a MediaWiki hook when a page is saved to trigger the Discord event. We probably need to create a hook for the ConfirmAccount extension for it to communicate with the Discord extension.<syntaxhighlight lang="php" start="16" line="1" highlight="5,44">
/**
* Called when a page is created or edited
* @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete
*/
public static function onPageSaveComplete( WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revision, EditResult $editResult ) {
global $wgDiscordNoBots, $wgDiscordNoMinor, $wgDiscordNoNull;
$hookName = 'PageContentSaveComplete';
    $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
 
if ( DiscordUtils::isDisabled( $hookName, $wikiPage->getTitle()->getNamespace(), $user ) ) {
return true;
}
 
if ( $wgDiscordNoBots && $user->isBot() ) {
// Don't continue, this is a bot edit
return true;
}
 
if ( $wgDiscordNoMinor && $revision->isMinor() ) {
// Don't continue, this is a minor edit
return true;
}
 
if ( $wgDiscordNoNull && $editResult->isNullEdit() ) {
// Don't continue, this is a null edit
return true;
}
 
$isNew = $editResult->isNew();
if ( $wikiPage->getTitle()->inNamespace( NS_FILE ) && $isNew ) {
// Don't continue, it's a new file which onUploadComplete will handle instead
return true;
}
 
$msgKey = 'discord-edit';
if ( $isNew ) { // is a new page
$msgKey = 'discord-create';
}
 
$msg = wfMessage( $msgKey, DiscordUtils::createUserLinks( $user ),
DiscordUtils::createMarkdownLink( $wikiPage->getTitle(), $wikiPage->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ),
DiscordUtils::createRevisionText( $revision ),
( $summary ? ('`' . DiscordUtils::sanitiseText( DiscordUtils::truncateText( $summary ) ) . '`' ) : '' ) )->plain();
DiscordUtils::handleDiscord($hookName, $msg);
return true;
}
</syntaxhighlight>
 
==== Utils.php ====
Further detailing of the "handleDiscord" (called on line 59 of Discord.php) method, which contains the logic for communication with Discord<syntaxhighlight lang="php" line="1" start="47">
/**
* Handles sending a webhook to Discord using cURL
*/
public static function handleDiscord ($hookName, $msg) {
global $wgDiscordWebhookURL, $wgDiscordEmojis, $wgDiscordUseEmojis, $wgDiscordPrependTimestamp, $wgDiscordUseFileGetContents;
 
if ( !$wgDiscordWebhookURL ) {
// There's nothing in here, so we won't do anything
return false;
}
 
$urls = [];
 
if ( is_array( $wgDiscordWebhookURL ) ) {
$urls = array_merge($urls, $wgDiscordWebhookURL);
} else if ( is_string($wgDiscordWebhookURL) ) {
$urls[] = $wgDiscordWebhookURL;
} else {
wfDebugLog( 'discord', 'The value of $wgDiscordWebhookURL is not valid and therefore no webhooks could be sent.' );
return false;
}
 
// Strip whitespace to just one space
$stripped = preg_replace('/\s+/', ' ', $msg);
 
if ( $wgDiscordPrependTimestamp ) {
// Add timestamp
$dateString = gmdate( wfMessage( 'discord-timestampformat' )->text() );
$stripped = $dateString . ' ' . $stripped;
}
 
if ( $wgDiscordUseEmojis ) {
// Add emoji
$emoji = $wgDiscordEmojis[$hookName];
$stripped = $emoji . ' ' . $stripped;
}
 
DeferredUpdates::addCallableUpdate( function() use ( $stripped, $urls, $wgDiscordUseFileGetContents ) {
$user_agent = 'mw-discord/1.0 (github.com/jaydenkieran)';
$json_data = [
'content' => "$stripped",
'allowed_mentions' => [
'parse' => []
]
];
$json = json_encode($json_data);
 
if ( $wgDiscordUseFileGetContents ) {
// They want to use file_get_contents
foreach ($urls as &$value) {
$contextOpts = [
'http' => [
'header' => 'Content-Type: application/x-www-form-urlencoded',
'method' => 'POST', // Send as a POST request
'user_agent' => $user_agent, // Add a unique user agent
'content' => $json, // Send the JSON in the POST request
'ignore_errors' => true // If the call fails, let's not do anything with it
]
];
 
$context = stream_context_create( $contextOpts );
$result = file_get_contents( $value, false, $context );
}
} else {
// By default, we use cURL
// Set up cURL multi handlers
$c_handlers = [];
$result = [];
$mh = curl_multi_init();
 
foreach ($urls as &$value) {
$c_handlers[$value] = curl_init( $value );
curl_setopt( $c_handlers[$value], CURLOPT_POST, 1 ); // Send as a POST request
curl_setopt( $c_handlers[$value], CURLOPT_POSTFIELDS, $json ); // Send the JSON in the POST request
curl_setopt( $c_handlers[$value], CURLOPT_FOLLOWLOCATION, 1 );
curl_setopt( $c_handlers[$value], CURLOPT_HEADER, 0 );
curl_setopt( $c_handlers[$value], CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $c_handlers[$value], CURLOPT_CONNECTTIMEOUT, 10 ); // Add a timeout for connecting to the site
curl_setopt( $c_handlers[$value], CURLOPT_TIMEOUT, 10 ); // Do not allow cURL to run for a long time
curl_setopt( $c_handlers[$value], CURLOPT_USERAGENT, $user_agent ); // Add a unique user agent
curl_setopt( $c_handlers[$value], CURLOPT_HTTPHEADER, array(
'Content-Type: application/json'
));
curl_multi_add_handle( $mh, $c_handlers[$value] );
}
 
$running = null;
do {
curl_multi_exec($mh, $running);
} while ($running);
 
// Remove all handlers and then close the multi handler
foreach($c_handlers as $k => $ch) {
$result[$k] = curl_multi_getcontent($ch);
wfDebugLog( 'discord', 'Result of cURL was: ' . $result[$k] );
curl_multi_remove_handle($mh, $ch);
}
 
curl_multi_close($mh);
}
} );
 
return true;
}
</syntaxhighlight>
 
=== Extension hooks used by Discord extension ===
The Discord extension uses two hooks declared by extensions, ApprovedRevs and Renameuser extension. Here are examples of the hooks used in the Discord extension, and their registry in each respective extension.
 
==== ApprovedRevs ====
<syntaxhighlight lang="php">
/**
* Called when a revision is approved (Approved Revs extension)
* @see https://github.com/wikimedia/mediawiki-extensions-ApprovedRevs/blob/REL1_34/includes/ApprovedRevs_body.php
*/
public static function onApprovedRevsRevisionApproved ( $output, $title, $rev_id, $content ) {
global $wgDiscordNoBots;
$hookName = 'ApprovedRevsRevisionApproved';
 
$user = RequestContext::getMain()->getUser();
 
if ( DiscordUtils::isDisabled( $hookName, $title->getNamespace(), $user ) ) {
return true;
}
 
if ( $wgDiscordNoBots && $user->isBot() ) {
// Don't continue, this is a bot
return true;
}
 
// Get the revision being approved here
$rev = MediaWikiServices::getInstance()->getRevisionLookup()->getRevisionByTitle( $title, $rev_id );
$revLink = $title->getFullURL( '', false, PROTO_CANONICAL );
$revAuthor = DiscordUtils::createUserLinks( $rev->getUser( RevisionRecord::RAW ) );
 
$msg = wfMessage( 'discord-approvedrevsrevisionapproved', DiscordUtils::createUserLinks( $user ),
DiscordUtils::createMarkdownLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
DiscordUtils::createMarkdownLink( $rev_id, $revLink ),
$revAuthor)->plain();
DiscordUtils::handleDiscord($hookName, $msg);
return true;
}
</syntaxhighlight>
 
This is how the ApprovedRevs extension register the hook used by Discord extension.<syntaxhighlight lang="php" start="488" highlight="26,27">
MediaWikiServices::getInstance()->getHookContainer()->run( 'ApprovedRevsRevisionApproved', [ $output = null, $title, $rev_id, $content ] );
</syntaxhighlight>
 
==== Renameuser ====
How the Discord extension calls the hooks:<syntaxhighlight lang="php">
/**
* Called when a user is renamed (Renameuser extension)
* @see https://github.com/wikimedia/mediawiki-extensions-Renameuser/blob/REL1_36/includes/RenameuserSQL.php
*/
public static function onRenameUserComplete ( $uid, $old, $new ) {
$hookName = 'RenameUserComplete';
 
$user = RequestContext::getMain()->getUser();
 
if ( DiscordUtils::isDisabled( $hookName, null, null ) ) {
return true;
}
 
    $renamedUserAsTitle = MediaWikiServices::getInstance()->getUserFactory()->newFromName( $new )->getUserPage();
 
$msg = wfMessage( 'discord-renameusercomplete', DiscordUtils::createUserLinks( $user ),
"*$old*",
DiscordUtils::createMarkdownLink( $new, $renamedUserAsTitle->getFullURL( '', false, PROTO_CANONICAL ) ) )->plain();
DiscordUtils::handleDiscord($hookName, $msg);
return true;
}
</syntaxhighlight>How the Renameuser extension registers that hook is a bit different and split into different classes.
 
On the constructor of the database handler, '''RenameuserSQL.php'''<syntaxhighlight lang="php">
$this->hookRunner = new RenameuserHookRunner( MediaWikiServices::getInstance()->getHookContainer() );
</syntaxhighlight>It calls the class '''RenameuserHookRunner.php''' with the container because passed to the constructor of that class, for some reason<syntaxhighlight lang="php">
public function __construct( HookContainer $container ) {
$this->container = $container;
}
</syntaxhighlight>Then inside there's a bunch of hook declarations. The one that interests us is this one<syntaxhighlight lang="php">
public function onRenameUserComplete( int $uid, string $old, string $new ): void {
$this->container->run(
'RenameUserComplete',
[ $uid, $old, $new ],
[ 'abortable' => false ]
);
}
</syntaxhighlight>This is the equivalent of<syntaxhighlight lang="php">
MediaWikiServices::getInstance()->getHookContainer()->run( 'RenameUserComplete', [ $uid, $old, $new ], [ 'abortable' => false ] );
</syntaxhighlight>
 
=== Current changes to ConfirmAccount and Discord extension ===
 
==== ConfirmAccount ====
<syntaxhighlight lang="php">
MediaWikiServices::getInstance()->getHookContainer()->run( 'ConfirmAccountRequestComplete', [ $userName=$this->name, $acr_id=$this->id ] );
</syntaxhighlight>
 
== Module pages in English and Portuguese instances produces errors ==
Full backtrace:
Full backtrace:
<pre>LogicException: This ParserOutput contains no text!
<pre>LogicException: This ParserOutput contains no text!
Line 39: Line 295:
return $this->mText;
return $this->mText;
}
}
</syntaxhighlight>
'''Further context on mText'''<syntaxhighlight lang="php" line="1" start="106">
private $mText = null;
</syntaxhighlight><syntaxhighlight lang="php" line="1" start="349">
public function __construct( $text = null, $languageLinks = [], $categoryLinks = [],
        $unused = false, $titletext = ''
) { 
        $this->mText = $text;
        $this->mLanguageLinks = $languageLinks;
        $this->mCategories = $categoryLinks;
        $this->mTitleText = $titletext;
}
</syntaxhighlight><syntaxhighlight lang="php" line="1" start="945">
public function setText( $text ) {
        return wfSetVar( $this->mText, $text, true );
}   
</syntaxhighlight>
</syntaxhighlight>
  #0 /var/www/prolewiki/extensions/Scribunto/includes/ScribuntoContentHandler.php(224): ParserOutput->getRawText()
  #0 /var/www/prolewiki/extensions/Scribunto/includes/ScribuntoContentHandler.php(224): ParserOutput->getRawText()

Latest revision as of 00:26, 30 April 2024

Notification system for ConfirmAccount extension

The following is to detail how the Discord extension works, in hopes to implement that on the ConfirmAccount extension.

Discord extension as reference for Discord notification

There are two main files (Discord.php and DiscordUtils.php) that serve the Discord extension, and together they compose the logic of the notification system.

Discord.php

This example function uses a MediaWiki hook when a page is saved to trigger the Discord event. We probably need to create a hook for the ConfirmAccount extension for it to communicate with the Discord extension.

/**
 * Called when a page is created or edited
 * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete
 */
public static function onPageSaveComplete( WikiPage $wikiPage, UserIdentity $userIdentity, string $summary, int $flags, RevisionRecord $revision, EditResult $editResult ) {
	global $wgDiscordNoBots, $wgDiscordNoMinor, $wgDiscordNoNull;
	$hookName = 'PageContentSaveComplete';
    $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );

	if ( DiscordUtils::isDisabled( $hookName, $wikiPage->getTitle()->getNamespace(), $user ) ) {
		return true;
	}

	if ( $wgDiscordNoBots && $user->isBot() ) {
		// Don't continue, this is a bot edit
		return true;
	}

	if ( $wgDiscordNoMinor && $revision->isMinor() ) {
		// Don't continue, this is a minor edit
		return true;
	}

	if ( $wgDiscordNoNull && $editResult->isNullEdit() ) {
		// Don't continue, this is a null edit
		return true;
	}

	$isNew = $editResult->isNew();
	if ( $wikiPage->getTitle()->inNamespace( NS_FILE ) && $isNew ) {
		// Don't continue, it's a new file which onUploadComplete will handle instead
		return true;
	}

	$msgKey = 'discord-edit';
	if ( $isNew ) { // is a new page
		$msgKey = 'discord-create';
	}

	$msg = wfMessage( $msgKey, DiscordUtils::createUserLinks( $user ),
		DiscordUtils::createMarkdownLink( $wikiPage->getTitle(), $wikiPage->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ),
		DiscordUtils::createRevisionText( $revision ),
		( $summary ? ('`' . DiscordUtils::sanitiseText( DiscordUtils::truncateText( $summary ) ) . '`' ) : '' ) )->plain();
	DiscordUtils::handleDiscord($hookName, $msg);
	return true;
}

Utils.php

Further detailing of the "handleDiscord" (called on line 59 of Discord.php) method, which contains the logic for communication with Discord

/**
 * Handles sending a webhook to Discord using cURL
 */
public static function handleDiscord ($hookName, $msg) {
	global $wgDiscordWebhookURL, $wgDiscordEmojis, $wgDiscordUseEmojis, $wgDiscordPrependTimestamp, $wgDiscordUseFileGetContents;

	if ( !$wgDiscordWebhookURL ) {
		// There's nothing in here, so we won't do anything
		return false;
	}

	$urls = [];

	if ( is_array( $wgDiscordWebhookURL ) ) {
		$urls = array_merge($urls, $wgDiscordWebhookURL);
	} else if ( is_string($wgDiscordWebhookURL) ) {
		$urls[] = $wgDiscordWebhookURL;
	} else {
		wfDebugLog( 'discord', 'The value of $wgDiscordWebhookURL is not valid and therefore no webhooks could be sent.' );
		return false;
	}

	// Strip whitespace to just one space
	$stripped = preg_replace('/\s+/', ' ', $msg);

	if ( $wgDiscordPrependTimestamp ) {
		// Add timestamp
		$dateString = gmdate( wfMessage( 'discord-timestampformat' )->text() );
		$stripped = $dateString . ' ' . $stripped;
	}

	if ( $wgDiscordUseEmojis ) {
		// Add emoji
		$emoji = $wgDiscordEmojis[$hookName];
		$stripped = $emoji . ' ' . $stripped;
	}

	DeferredUpdates::addCallableUpdate( function() use ( $stripped, $urls, $wgDiscordUseFileGetContents ) {
		$user_agent = 'mw-discord/1.0 (github.com/jaydenkieran)';
		$json_data = [
			'content' => "$stripped",
			'allowed_mentions' => [
				'parse' => []
			]
		];
		$json = json_encode($json_data);

		if ( $wgDiscordUseFileGetContents ) {
			// They want to use file_get_contents
			foreach ($urls as &$value) {
				$contextOpts = [
					'http' => [
						'header' => 'Content-Type: application/x-www-form-urlencoded',
						'method' => 'POST', // Send as a POST request
						'user_agent' => $user_agent, // Add a unique user agent
						'content' => $json, // Send the JSON in the POST request
						'ignore_errors' => true // If the call fails, let's not do anything with it
					]
				];

				$context = stream_context_create( $contextOpts );
				$result = file_get_contents( $value, false, $context );
			}
		} else {
			// By default, we use cURL
			// Set up cURL multi handlers
			$c_handlers = [];
			$result = [];
			$mh = curl_multi_init();

			foreach ($urls as &$value) {
				$c_handlers[$value] = curl_init( $value );
				curl_setopt( $c_handlers[$value], CURLOPT_POST, 1 ); // Send as a POST request
				curl_setopt( $c_handlers[$value], CURLOPT_POSTFIELDS, $json ); // Send the JSON in the POST request
				curl_setopt( $c_handlers[$value], CURLOPT_FOLLOWLOCATION, 1 );
				curl_setopt( $c_handlers[$value], CURLOPT_HEADER, 0 );
				curl_setopt( $c_handlers[$value], CURLOPT_RETURNTRANSFER, 1 );
				curl_setopt( $c_handlers[$value], CURLOPT_CONNECTTIMEOUT, 10 ); // Add a timeout for connecting to the site
				curl_setopt( $c_handlers[$value], CURLOPT_TIMEOUT, 10 ); // Do not allow cURL to run for a long time
				curl_setopt( $c_handlers[$value], CURLOPT_USERAGENT, $user_agent ); // Add a unique user agent
				curl_setopt( $c_handlers[$value], CURLOPT_HTTPHEADER, array(
					'Content-Type: application/json'
				));
				curl_multi_add_handle( $mh, $c_handlers[$value] );
			}

			$running = null;
			do {
				curl_multi_exec($mh, $running);
			} while ($running);

			// Remove all handlers and then close the multi handler
			foreach($c_handlers as $k => $ch) {
				$result[$k] = curl_multi_getcontent($ch);
				wfDebugLog( 'discord', 'Result of cURL was: ' . $result[$k] );
				curl_multi_remove_handle($mh, $ch);
			}

			curl_multi_close($mh);
		}
	} );

	return true;
}

Extension hooks used by Discord extension

The Discord extension uses two hooks declared by extensions, ApprovedRevs and Renameuser extension. Here are examples of the hooks used in the Discord extension, and their registry in each respective extension.

ApprovedRevs

/**
 * Called when a revision is approved (Approved Revs extension)
 * @see https://github.com/wikimedia/mediawiki-extensions-ApprovedRevs/blob/REL1_34/includes/ApprovedRevs_body.php
 */
public static function onApprovedRevsRevisionApproved ( $output, $title, $rev_id, $content ) {
	global $wgDiscordNoBots;
	$hookName = 'ApprovedRevsRevisionApproved';

	$user = RequestContext::getMain()->getUser();

	if ( DiscordUtils::isDisabled( $hookName, $title->getNamespace(), $user ) ) {
		return true;
	}

	if ( $wgDiscordNoBots && $user->isBot() ) {
		// Don't continue, this is a bot
		return true;
	}

	// Get the revision being approved here
	$rev = MediaWikiServices::getInstance()->getRevisionLookup()->getRevisionByTitle( $title, $rev_id );
	$revLink = $title->getFullURL( '', false, PROTO_CANONICAL );
	$revAuthor = DiscordUtils::createUserLinks( $rev->getUser( RevisionRecord::RAW ) );

	$msg = wfMessage( 'discord-approvedrevsrevisionapproved', DiscordUtils::createUserLinks( $user ),
		DiscordUtils::createMarkdownLink( $title, $title->getFullURL( '', false, PROTO_CANONICAL ) ),
		DiscordUtils::createMarkdownLink( $rev_id, $revLink ),
		$revAuthor)->plain();
	DiscordUtils::handleDiscord($hookName, $msg);
	return true;
}

This is how the ApprovedRevs extension register the hook used by Discord extension.

MediaWikiServices::getInstance()->getHookContainer()->run( 'ApprovedRevsRevisionApproved', [ $output = null, $title, $rev_id, $content ] );

Renameuser

How the Discord extension calls the hooks:

/**
 * Called when a user is renamed (Renameuser extension)
 * @see https://github.com/wikimedia/mediawiki-extensions-Renameuser/blob/REL1_36/includes/RenameuserSQL.php
 */
public static function onRenameUserComplete ( $uid, $old, $new ) {
	$hookName = 'RenameUserComplete';

	$user = RequestContext::getMain()->getUser();

	if ( DiscordUtils::isDisabled( $hookName, null, null ) ) {
		return true;
	}

    $renamedUserAsTitle = MediaWikiServices::getInstance()->getUserFactory()->newFromName( $new )->getUserPage();

	$msg = wfMessage( 'discord-renameusercomplete', DiscordUtils::createUserLinks( $user ),
		"*$old*",
		DiscordUtils::createMarkdownLink( $new, $renamedUserAsTitle->getFullURL( '', false, PROTO_CANONICAL ) ) )->plain();
		DiscordUtils::handleDiscord($hookName, $msg);
	return true;
}

How the Renameuser extension registers that hook is a bit different and split into different classes. On the constructor of the database handler, RenameuserSQL.php

$this->hookRunner = new RenameuserHookRunner( MediaWikiServices::getInstance()->getHookContainer() );

It calls the class RenameuserHookRunner.php with the container because passed to the constructor of that class, for some reason

public function __construct( HookContainer $container ) {
	$this->container = $container;
}

Then inside there's a bunch of hook declarations. The one that interests us is this one

public function onRenameUserComplete( int $uid, string $old, string $new ): void {
	$this->container->run(
		'RenameUserComplete',
		[ $uid, $old, $new ],
		[ 'abortable' => false ]
	);
}

This is the equivalent of

MediaWikiServices::getInstance()->getHookContainer()->run( 'RenameUserComplete', [ $uid, $old, $new ], [ 'abortable' => false ] );

Current changes to ConfirmAccount and Discord extension

ConfirmAccount

MediaWikiServices::getInstance()->getHookContainer()->run( 'ConfirmAccountRequestComplete', [ $userName=$this->name, $acr_id=$this->id ] );

Module pages in English and Portuguese instances produces errors

Full backtrace:

LogicException: This ParserOutput contains no text!

Backtrace:

from /var/www/prolewiki/includes/parser/ParserOutput.php(382)
#0 /var/www/prolewiki/extensions/Scribunto/includes/ScribuntoContentHandler.php(224): ParserOutput->getRawText()
#1 /var/www/prolewiki/extensions/Scribunto/includes/ScribuntoContentHandler.php(193): MediaWiki\Extension\Scribunto\ScribuntoContentHandler->highlight()
#2 /var/www/prolewiki/includes/content/ContentHandler.php(1759): MediaWiki\Extension\Scribunto\ScribuntoContentHandler->fillParserOutput()
#3 /var/www/prolewiki/includes/content/Renderer/ContentRenderer.php(47): ContentHandler->getParserOutput()
#4 /var/www/prolewiki/includes/Revision/RenderedRevision.php(259): MediaWiki\Content\Renderer\ContentRenderer->getParserOutput()
#5 /var/www/prolewiki/includes/Revision/RenderedRevision.php(232): MediaWiki\Revision\RenderedRevision->getSlotParserOutputUncached()
#6 /var/www/prolewiki/includes/Revision/RevisionRenderer.php(223): MediaWiki\Revision\RenderedRevision->getSlotParserOutput()
#7 /var/www/prolewiki/includes/Revision/RevisionRenderer.php(164): MediaWiki\Revision\RevisionRenderer->combineSlotOutput()
#8 [internal function]: MediaWiki\Revision\RevisionRenderer->MediaWiki\Revision\{closure}()
#9 /var/www/prolewiki/includes/Revision/RenderedRevision.php(199): call_user_func()
#10 /var/www/prolewiki/includes/poolcounter/PoolWorkArticleView.php(84): MediaWiki\Revision\RenderedRevision->getRevisionParserOutput()
#11 /var/www/prolewiki/includes/poolcounter/PoolWorkArticleView.php(69): PoolWorkArticleView->renderRevision()
#12 /var/www/prolewiki/includes/poolcounter/PoolCounterWork.php(167): PoolWorkArticleView->doWork()
#13 /var/www/prolewiki/includes/page/ParserOutputAccess.php(304): PoolCounterWork->execute()
#14 /var/www/prolewiki/includes/page/Article.php(746): MediaWiki\Page\ParserOutputAccess->getParserOutput()
#15 /var/www/prolewiki/includes/page/Article.php(550): Article->generateContentOutput()
#16 /var/www/prolewiki/includes/actions/ViewAction.php(78): Article->view()
#17 /var/www/prolewiki/includes/MediaWiki.php(583): ViewAction->show()
#18 /var/www/prolewiki/includes/MediaWiki.php(363): MediaWiki->performAction()
#19 /var/www/prolewiki/includes/MediaWiki.php(960): MediaWiki->performRequest()
#20 /var/www/prolewiki/includes/MediaWiki.php(613): MediaWiki->main()
#21 /var/www/prolewiki/index.php(50): MediaWiki->run()
#22 /var/www/prolewiki/index.php(46): wfIndexMain()
#23 {main}
from /var/www/prolewiki/includes/parser/ParserOutput.php (382)
public function getRawText() {
		if ( $this->mText === null ) {
			throw new LogicException( 'This ParserOutput contains no text!' );
		}

		return $this->mText;
}

Further context on mText

private $mText = null;
public function __construct( $text = null, $languageLinks = [], $categoryLinks = [],
        $unused = false, $titletext = '' 
) {  
        $this->mText = $text;
        $this->mLanguageLinks = $languageLinks;
        $this->mCategories = $categoryLinks;
        $this->mTitleText = $titletext;
}
public function setText( $text ) {
        return wfSetVar( $this->mText, $text, true );
}
#0 /var/www/prolewiki/extensions/Scribunto/includes/ScribuntoContentHandler.php(224): ParserOutput->getRawText()
protected function highlight( $text, ParserOutput $parserOutput, ScribuntoEngineBase $engine ) {
    global $wgScribuntoUseGeSHi;
    $language = $engine->getGeSHiLanguage();
    if (
        $wgScribuntoUseGeSHi && $language && ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' )
    ) {
        $status = SyntaxHighlight::highlight( $text, $language, [ 'line' => true, 'linelinks' => 'L' ] );
        if ( $status->isGood() ) {
            // @todo replace addModuleStyles line with the appropriate call on
            // SyntaxHighlight once one is created
            $parserOutput->addModuleStyles( [ 'ext.pygments' ] );
            $parserOutput->addModules( [ 'ext.pygments.linenumbers' ] );
            $parserOutput->setText( $parserOutput->getRawText() . $status->getValue() );
            return true;
        }
    }
    return false;
}