Your IP : 216.73.216.54


Current Path : /var/www/html/mediawiki-1.43.1/extensions/EmailNotifications/includes/
Upload File :
Current File : /var/www/html/mediawiki-1.43.1/extensions/EmailNotifications/includes/EmailNotifications.php

<?php
/**
 * This file is part of the MediaWiki extension EmailNotifications.
 *
 * EmailNotifications is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * v is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with EmailNotifications.  If not, see <https://www.gnu.org/licenses/>.
 *
 * @file
 * @ingroup extensions
 * @author thomas-topway-it <support@topway.it>
 * @copyright Copyright ©2024, https://wikisphere.org
 */

use MediaWiki\Extension\EmailNotifications\BodyPostProcess;
use MediaWiki\Extension\EmailNotifications\Mailer;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use Psr\Log\LoggerInterface;

class EmailNotifications {

	/** @var array */
	public static $cacheIdSubject = [];

	/** @var array */
	public static $cacheSubjectId = [];

	/** @var LoggerInterface */
	public static $Logger;

	/** @var array */
	public static $UserAuthCache = [];

	/**
	 * @param string $creatorUsername
	 * @param array $row
	 * @param int|null $id
	 * @return bool
	 */
	public static function setNotifications( $creatorUsername, $row, $id = null ) {
		$dbw = self::getDB( DB_MASTER );

		if ( !count( $row['groups'] ) ) {
			return false;
		}

		$row['groups'] = implode( ',', $row['groups'] );
		$row['created_by'] = $creatorUsername;

		$tablename = 'emailnotifications_notifications';

		if ( !$id ) {
			$date = date( 'Y-m-d H:i:s' );
			$res = $dbw->insert( $tablename, $row + [ 'updated_at' => $date, 'created_at' => $date ] );

		} else {
			$res = $dbw->update( $tablename, $row, [ 'id' => $id ], __METHOD__ );
		}

		return $res;
	}

	/**
	 * @param array $headers
	 * @param array $to
	 * @param MailAddress $from
	 * @param string $subject
	 * @param string $text
	 * @param string $html
	 * @param array $attachments
	 * @param array &$errors
	 * @return bool
	 */
	public static function sendEmail( $headers, $to, $from, $subject, $text, $html, $attachments = [], &$errors = [] ) {
		$mailer = $GLOBALS['wgEmailNotificationsMailer'];
		$conf = $GLOBALS['wgEmailNotificationsMailerConf'];

		if ( empty( $mailer ) || empty( $conf ) ) {
			return false;
		}

		$mailer = new Mailer( $mailer, $conf );
		$errors_ = $mailer->getErrors();

		if ( count( $errors_ ) ) {
			$errors = array_merge( $errors, $errors_ );
			return false;
		}

		$email = $mailer->mail;

		if ( !empty( $headers ) ) {
			$headersEmail = $email->getHeaders();
			$ignore = [ 'From', 'Return-Path', 'Date', 'Message-ID',
				'MIME-Version', 'Content-type', 'content-transfer-encoding' ];

			foreach ( $headers as $key => $value ) {

				// automatically assigned
				if ( in_array( $key, $ignore ) ) {
					continue;
				}

				// @see vendor/symfony/mime/Header/Headers.php
				switch ( strtolower( $key ) ) {
					case 'date':
						$headersEmail->addDateHeader( $key, new \DateTimeImmutable() );
						break;
					case 'from':
					case 'to':
					case 'cc':
					case 'bcc':
					// @see https://www.mediawiki.org/w/index.php?title=Topic:Yh239sott8bbkc0e&topic_showPostId=yh4ksi74qd0vhlf4#flow-post-yh4ksi74qd0vhlf4
					case 'reply-to':
						$headersEmail->addMailboxListHeader( $key, $value );
						break;
					case 'sender':
						$headersEmail->addMailboxHeader( $key, $value );
						break;
					case 'message-id':
						$headersEmail->addIdHeader( $key, $value );
						break;
					case 'return-path':
						$headersEmail->addPathHeader( $key, $value );
						break;
					default:
						$headersEmail->addTextHeader( $key, $value );
				}
			}
		}

		$email->from( !empty( $from->name ) ? $from->name . '<' . $from->address . '>'
			: $from->address );

		if ( empty( $subject ) ) {
			// zero width space, this is a workaround
			// for the annoying error
			// 'Unable to send an email: The subject is required'
			$subject = '​';
		}

		$email->subject( $subject );

		if ( !empty( $html ) ) {
			$headersEmail->addTextHeader( 'Content-type', 'text/html; charset=UTF-8' );

			$bodyPostProcess = new BodyPostProcess( $GLOBALS['wgServer'], $html );
			$bodyPostProcess->updateImageUrls();
			$html = $bodyPostProcess->getHtml();

			$email->html( $html );

			if ( empty( $text ) ) {
				$text = $mailer->html2Text( $html );
			}
		}

		if ( !empty( $text ) ) {
			$email->text( $text );
		}

		$email->to( implode( ', ', $to ) );

		// @see https://phpenterprisesystems.com/symfony-framework/93-how-to-send-emails-with-attachments-in-symfony-6
		foreach ( $attachments as $value ) {
			$email->attach( $value['body'], $value['name'], $value['contentType'] );
		}

		$mailer->sendEmail( $email );

		return true;
	}

	/**
	 * @param array $groups
	 * @param array $errors
	 * @return array|bool
	 */
	public static function usersInGroups( $groups, $errors = [] ) {
		$context = RequestContext::getMain();

		// @see https://www.mediawiki.org/wiki/API:Allusers
		$row = [
			'action' => 'query',
			'list' => 'allusers',
			'augroup' => implode( '|', $groups )
		];

		$req = new DerivativeRequest(
			$context->getRequest(),
			$row,
			true
		);

		try {
			$api = new ApiMain( $req, true );
			$api->execute();

		} catch ( \Exception $e ) {
			$errors[] = 'api error ' . $e->getMessage();
			self::$Logger->error( current( $errors ) );
			return false;
		}

		$res = $api->getResult()->getResultData();
		$ret = [];
		if ( !empty( $res['query']['allusers'] ) ) {
			foreach ( $res['query']['allusers'] as $value ) {
				if ( is_array( $value ) ) {
					$ret[] = $value['userid'];
				}
			}
		}
		return $ret;
	}

	/**
	 * @param int $notificationId
	 * @param array $groups
	 * @param int $page
	 * @param string $subject
	 * @param bool $must_differ
	 * @param string $skip_strategy
	 * @param string $skip_text
	 * @param array &$errors
	 * @return array|bool
	 */
	public static function sendNotification(
		$notificationId,
		$groups,
		$page,
		$subject,
		$must_differ,
		$skip_strategy,
		$skip_text,
		&$errors = []
	) {
		$users = self::usersInGroups( $groups );

		if ( !count( $users ) ) {
			$errors[] = 'no recipients';
			self::$Logger->warning( current( $errors ) );
			return false;
		}

		$title_ = Title::newFromId( $page );
		$wikiPage = self::getWikiPage( $title_ );

		if ( !$wikiPage ) {
			$errors[] = 'article not valid';
			self::$Logger->error( current( $errors ) );
			return false;
		}

		$wikiPage->doPurge();

		$options = [
			'allowTOC' => false,
			'injectTOC' => false,
			'enableSectionEditLinks' => false,
			'userLang' => null,
			'skin' => null,
			'unwrap' => true,
			// 'wrapperDivClass' => $this->getWrapperDivClass(),
			'deduplicateStyles' => true,
			'absoluteURLs' => true,
			'includeDebugInfo' => false,
			'bodyContentOnly' => true,
		];
		$context = RequestContext::getMain();
		$context->setTitle( $title_ );
		$parserOptions = ParserOptions::newFromContext( $context );
		$parserOutput = $wikiPage->getParserOutput( $parserOptions );

		$html = Parser::stripOuterParagraph( $parserOutput->getText( $options ) );

		$html2Text = new \Html2Text\Html2Text( $html );
		$text = $html2Text->getText();

		if ( !empty( $skip_text ) ) {
			switch ( $skip_strategy ) {
				case 'contains':
					if ( strpos( $text, $skip_text ) !== false ) {
						$errors[] = 'skip text contains';
						self::$Logger->warning( current( $errors ) );
						return false;
					}
					break;
				case 'does not contain':
					if ( strpos( $text, $skip_text ) === false ) {
						$errors[] = 'skip text does not contain';
						self::$Logger->warning( current( $errors ) );
						return false;
					}
					break;
				case 'regex':
					if ( preg_match( '/' . str_replace( '/', '\/', $skip_text ) . '/', $text ) ) {
						$errors[] = 'skip text regex';
						self::$Logger->warning( current( $errors ) );
						return false;
					}
			}
		}

		$date = date( 'Y-m-d H:i:s' );
		$dbr = self::getDb( DB_REPLICA );

		if ( $must_differ ) {
			$tablename = 'emailnotifications_sent';
			$previous_text = $dbr->selectField(
				$tablename,
				'text',
				[ 'notification_id' => $notificationId ],
				__METHOD__,
				[ 'LIMIT' => 1, 'ORDER BY' => 'created_at DESC' ],
			);
			if ( $previous_text && $previous_text === $text ) {
				$errors[] = 'text does not differ';
				self::$Logger->warning( current( $errors ) );
				return false;
			}
		}

		$services = MediaWikiServices::getInstance();
		$passwordSender = $services->getMainConfig()
			->get( MainConfigNames::PasswordSender );

		if ( empty( $passwordSender ) ) {
			$errors[] = '$wgPasswordSender not set';
			self::$Logger->warning( current( $errors ) );
			return false;
		}

		$sender = new MailAddress( $passwordSender,
			wfMessage( 'emailsender' )->inContentLanguage()->text() );

		$userFactory = $services->getUserFactory();
		$userOptionsLookup = $services->getUserOptionsLookup();
		$languageFactory = $services->getLanguageFactory();

		$parser = $services->getParserFactory()->getInstance();
		$parser->setTitle( $title_ );

		$sent = [];
		foreach ( $users as $userid ) {
			$user = $userFactory->newFromId( $userid );
			if ( !$user ) {
				continue;
			}
			$language = $userOptionsLookup->getOption( $user, 'language' );
			$parserOptions = ParserOptions::newFromUserAndLang( $user, $languageFactory->getLanguage( $language ) );
			$parserOutput = $wikiPage->getParserOutput( $parserOptions );

			$html = Parser::stripOuterParagraph( $parserOutput->getText( $options ) );

			// parse subject as wikitext
			$parser->setOptions( $parserOptions );
			$parser->setOutputType( Parser::OT_PLAIN );
			$parser->clearState();
			$subject_ = $parser->recursiveTagParseFully( $subject );
			$html2Text_ = new \Html2Text\Html2Text( $subject_ );
			$subject_ = $html2Text_->getText();

			$email = $user->getEmail();
			if ( $email ) {
				$to = new MailAddress(
					$user->getEmail(),
					$user->getName(),
					$user->getRealName()
				);

				$tablename = 'emailnotifications_unsubscribe';
				$unsubscribed = $dbr->selectField(
					$tablename,
					'notification_id',
					[ 'notification_id' => $notificationId, 'user_id' => $userid ],
					__METHOD__,
					[ 'LIMIT' => 1 ],
				);

				if ( $unsubscribed ) {
					continue;
				}

				$body = $html;
				$url = SpecialPage::getTitleFor( 'EmailNotificationsEvents', $notificationId )
					->getFullURL( '', false, PROTO_CANONICAL );
				$listUnsubscribe = '<' . wfAppendQuery( $url, [ 'action' => 'unsubscribe' ] ) . '>';
				$trackingUrl = wfAppendQuery( $url, [ 'action' => 'tracking', 'msgId' =>
					str_replace( [ '<', '>' ], '', self::makeMsgId( $notificationId, $userid, $date ) ) ] );

				UserMailer::send( $to, $sender, $subject_, $body, [
					 'headers' => [
						// 'List-Unsubscribe' will be overwritten
						'EmailNotifications-ListUnsubscribe' => $listUnsubscribe,
						'EmailNotifications-TrackingUrl' => $trackingUrl,
					 ],
				] );
				$sent[] = $userid;
			}
		}

		$tablename = 'emailnotifications_sent';
		$row = [
			'notification_id' => $notificationId,
			'text' => $text,
			'recipients' => count( $sent ),
		];
		$dbw = self::getDB( DB_PRIMARY );
		$options = [ 'IGNORE' ];
		$res = $dbw->insert(
			$tablename,
			$row + [ 'created_at' => $date ],
			__METHOD__,
			$options
		);

		return $sent;
	}

	/**
	 * @param string $messageId
	 * @return array
	 */
	public static function parseMessageId( $messageId ) {
		$messageId = substr( $messageId, 0, strpos( $messageId, '@' ) );
		return explode( '|', base64_decode( $messageId ) );
	}

	/**
	 * @see UserMailer::makeMsgId
	 * @param int $notificationId
	 * @param int $userid
	 * @param string $datetime
	 * @return string
	 */
	public static function makeMsgId( $notificationId, $userid, $datetime ) {
		$mainConfig = MediaWikiServices::getInstance()->getMainConfig();
		$smtp = $mainConfig->get( MainConfigNames::SMTP );
		$server = $mainConfig->get( MainConfigNames::Server );
		// $domainId = WikiMap::getCurrentWikiDbDomain()->getId();
		// $msgid = uniqid( $domainId . ".", true /** for cygwin */ );
		$msgid = base64_encode( "$notificationId|$userid|$datetime" );
		if ( is_array( $smtp ) && isset( $smtp['IDHost'] ) && $smtp['IDHost'] ) {
			$domain = $smtp['IDHost'];
		} else {
			$url = wfParseUrl( $server );
			$domain = $url['host'];
		}
		return "<$msgid@$domain>";
	}

	/**
	 * @param array $conds
	 * @return void
	 */
	public static function deleteItem( $conds ) {
		$dbw = self::getDB( DB_PRIMARY );
		$tablename = 'emailnotifications_notifications';
		$dbw->delete( $tablename, $conds, __METHOD__ );

		$tablename = 'emailnotifications_unsubscribe';
		$conds = [ 'notification_id' => $conds['id'] ];
		$dbw->delete( $tablename, $conds, __METHOD__ );
	}

	/**
	 * @param int $userId
	 * @param int $notificationId
	 * @return string|bool
	 */
	public static function unsubscribe( $userId, $notificationId ) {
		$dbw = self::getDB( DB_PRIMARY );
		$tablename = 'emailnotifications_unsubscribe';
		$row = [
			'notification_id' => $notificationId,
			'user_id' => $userId,
		];
		$date = date( 'Y-m-d H:i:s' );
		$options = [ 'IGNORE' ];
		$res = $dbw->insert(
			$tablename,
			$row + [ 'updated_at' => $date, 'created_at' => $date ],
			__METHOD__,
			$options
		);

		return self::getNotificationSubject( $notificationId );
	}

	/**
	 * @param int $notificationId
	 * @return string
	 */
	public static function getNotificationSubject( $notificationId ) {
		if ( array_key_exists( $notificationId, self::$cacheIdSubject ) ) {
			return self::$cacheIdSubject[$notificationId];
		}
		$dbr = self::getDb( DB_REPLICA );
		$tablename = 'emailnotifications_notifications';
		$subject = $dbr->selectField(
			$tablename,
			'subject',
			[ 'id' => $notificationId ],
			__METHOD__,
			[ 'LIMIT' => 1 ],
		);
		self::$cacheIdSubject[$notificationId] = $subject;
		return $subject;
	}

	/**
	 * @param string $subject
	 * @return int
	 */
	public static function notificationIdFromSubject( $subject ) {
		if ( array_key_exists( $subject, self::$cacheSubjectId ) ) {
			return self::$cacheSubjectId[$subject];
		}
		$dbr = self::getDb( DB_REPLICA );
		$tablename = 'emailnotifications_notifications';
		$id = (int)$dbr->selectField(
			$tablename,
			'id',
			[ 'subject' => $subject ],
			__METHOD__,
			[ 'LIMIT' => 1 ]
		);
		self::$cacheSubjectId[$subject] = $id;
		return $id;
	}

	/**
	 * @param User $user
	 * @return bool
	 */
	public static function isAuthorizedGroup( $user ) {
		$cacheKey = $user->getName();
		if ( array_key_exists( $cacheKey, self::$UserAuthCache ) ) {
			return self::$UserAuthCache[$cacheKey];
		}
		$userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
		$userGroups = $userGroupManager->getUserEffectiveGroups( $user );
		$authorizedGroups = [
			'sysop',
			'bureaucrat',
			'interface-admin',
			'autoconfirmed'
		];
		self::$UserAuthCache[$cacheKey] = count( array_intersect( $authorizedGroups, $userGroups ) );
		return self::$UserAuthCache[$cacheKey];
	}

	/**
	 * @param Title $title
	 * @return WikiPage|null
	 */
	public static function getWikiPage( $title ) {
		if ( !$title || !$title->canExist() ) {
			return null;
		}
		// MW 1.36+
		if ( method_exists( MediaWikiServices::class, 'getWikiPageFactory' ) ) {
			return MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
		}
		return WikiPage::factory( $title );
	}

	/**
	 * @param int $db
	 * @return \Wikimedia\Rdbms\DBConnRef
	 */
	public static function getDB( $db ) {
		if ( !method_exists( MediaWikiServices::class, 'getConnectionProvider' ) ) {
			// @see https://gerrit.wikimedia.org/r/c/mediawiki/extensions/PageEncryption/+/1038754/comment/4ccfc553_58a41db8/
			return MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( $db );
		}
		$connectionProvider = MediaWikiServices::getInstance()->getConnectionProvider();
		switch ( $db ) {
			case DB_PRIMARY:
			case DB_MASTER:
				return $connectionProvider->getPrimaryDatabase();
			case DB_REPLICA:
			default:
				return $connectionProvider->getReplicaDatabase();
		}
	}

}