ProleWiki:Tasks/Technical: Difference between revisions

From ProleWiki, the proletarian encyclopedia
(Added further context for mText variable)
Tag: Visual edit
(Detailed the functioning of the Discord extension, so that we can study it and implement for ConfirmAccount)
Tag: Visual edit
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.php ===
Uses a MediaWiki hook to trigger the Discord event. So we'd need to check for ConfirmAccount hooks, or worse, create one.<syntaxhighlight lang="php" start="16" line="1">
/**
* 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" 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>
 
== 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!

Revision as of 03:00, 29 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.php

Uses a MediaWiki hook to trigger the Discord event. So we'd need to check for ConfirmAccount hooks, or worse, create one.

/**
 * 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" 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;
}

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;
}