<commit_before><?php
namespace wcf\system\user\notification;
use wcf\data\object\type\ObjectType;
use wcf\data\object\type\ObjectTypeCache;
use wcf\data\user\notification\event\recipient\UserNotificationEventRecipientList;
use wcf\data\user\notification\event\UserNotificationEventList;
use wcf\data\user\notification\UserNotification;
use wcf\data\user\notification\UserNotificationAction;
use wcf\data\user\User;
use wcf\data\user\UserEditor;
use wcf\data\user\UserProfile;
use wcf\system\cache\builder\UserNotificationEventCacheBuilder;
use wcf\system\cache\runtime\UserProfileRuntimeCache;
use wcf\system\database\util\PreparedStatementConditionBuilder;
use wcf\system\email\mime\MimePartFacade;
use wcf\system\email\mime\RecipientAwareTextMimePart;
use wcf\system\email\Email;
use wcf\system\email\UserMailbox;
use wcf\system\event\EventHandler;
use wcf\system\exception\SystemException;
use wcf\system\request\LinkHandler;
use wcf\system\user\notification\event\IUserNotificationEvent;
use wcf\system\user\notification\object\type\IUserNotificationObjectType;
use wcf\system\user\notification\object\IUserNotificationObject;
use wcf\system\user\storage\UserStorageHandler;
use wcf\system\SingletonFactory;
use wcf\system\WCF;

/**
 * Handles user notifications.
 * 
 * @author	<NAME>, <NAME>
 * @copyright	2001-2019 WoltLab GmbH, <NAME>
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @package	WoltLabSuite\Core\System\User\Notification
 */
class UserNotificationHandler extends SingletonFactory {
	/**
	 * list of available object types
	 * @var	IUserNotificationObjectType[]
	 */
	protected $availableObjectTypes = [];
	
	/**
	 * list of available events
	 * @var	IUserNotificationEvent[][]
	 */
	protected $availableEvents = [];
	
	/**
	 * number of outstanding notifications
	 * @var	integer
	 */
	protected $notificationCount = null;
	
	/**
	 * list of object types
	 * @var	ObjectType[]
	 */
	protected $objectTypes = [];
	
	/**
	 * @inheritDoc
	 */
	protected function init() {
		// get available object types
		$this->objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.notification.objectType');
		foreach ($this->objectTypes as $typeName => $object) {
			$this->availableObjectTypes[$typeName] = $object->getProcessor();
		}
		
		// get available events
		$this->availableEvents = UserNotificationEventCacheBuilder::getInstance()->getData();
	}
	
	/**
	 * Triggers a notification event.
	 * 
	 * @param	string				$eventName
	 * @param	string				$objectType
	 * @param	IUserNotificationObject		$notificationObject
	 * @param	integer[]			$recipientIDs
	 * @param	mixed[]				$additionalData
	 * @param	integer				$baseObjectID
	 * @throws	SystemException
	 */
	public function fireEvent($eventName, $objectType, IUserNotificationObject $notificationObject, array $recipientIDs, array $additionalData = [], $baseObjectID = 0) {
		// check given object type and event name
		if (!isset($this->availableEvents[$objectType][$eventName])) {
			throw new SystemException("Unknown event ".$objectType."-".$eventName." given");
		}
		
		// get objects
		$objectTypeObject = $this->availableObjectTypes[$objectType];
		$event = $this->availableEvents[$objectType][$eventName];
		
		// get author's profile
		$userProfile = null;
		if ($notificationObject->getAuthorID()) {
			if ($notificationObject->getAuthorID() == WCF::getUser()->userID) {
				$userProfile = new UserProfile(WCF::getUser());
			}
			else {
				$userProfile = UserProfileRuntimeCache::getInstance()->getObject($notificationObject->getAuthorID());
			}
		}
		if ($userProfile === null) {
			$userProfile = new UserProfile(new User(null, []));
		}
		
		// set object data
		$event->setObject(new UserNotification(null, []), $notificationObject, $userProfile, $additionalData);
		$event->setAuthors([$event->getAuthorID() => $event->getAuthor()]);
		
		$parameters = [
			'eventName' => $eventName,
			'objectType' => $objectType,
			'notificationObject' => $notificationObject,
			'recipientIDs' => $recipientIDs,
			'additionalData' => $additionalData,
			'baseObjectID' => $baseObjectID,
			'objectTypeObject' => $objectTypeObject,
			'userProfile' => $userProfile,
			'event' => $event
		];
		// @deprecated 5.2 This event exposes incomplete data and should not be used, please use the following events instead.
		EventHandler::getInstance()->fireAction($this, 'fireEvent', $parameters);
		
		// find existing notifications
		$conditions = new PreparedStatementConditionBuilder();
		$conditions->add("userID IN (?)", [$recipientIDs]);
		$conditions->add("eventID = ?", [$event->eventID]);
		$conditions->add("eventHash = ?", [$event->getEventHash()]);
		$conditions->add("confirmTime = ?", [0]);
		
		$sql = "SELECT	notificationID, userID
			FROM	wcf".WCF_N."_user_notification
			".$conditions;
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute($conditions->getParameters());
		$notifications = $statement->fetchMap('userID', 'notificationID');
		
		// check if event supports stacking and author should be added
		if (!empty($notifications) && $event->isStackable()) {
			$conditions = new PreparedStatementConditionBuilder();
			$conditions->add("notificationID IN (?)", [array_values($notifications)]);
			if ($notificationObject->getAuthorID()) {
				$conditions->add("authorID = ?", [$notificationObject->getAuthorID()]);
			}
			else {
				$conditions->add("authorID IS NULL");
			}
			
			$sql = "SELECT	notificationID
				FROM	wcf".WCF_N."_user_notification_author
				".$conditions;
			$statement = WCF::getDB()->prepareStatement($sql);
			$statement->execute($conditions->getParameters());
			$notificationIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
			
			// filter array of existing notifications and remove values which
			// do not have a notification from this author yet (inverse logic!)
			foreach ($notifications as $userID => $notificationID) {
				if (!in_array($notificationID, $notificationIDs)) {
					unset($notifications[$userID]);
				}
			}
			
			if (!empty($notificationIDs)) {	
				// update trigger count
				$sql = "UPDATE	wcf".WCF_N."_user_notification
					SET	timesTriggered = timesTriggered + ?,
						guestTimesTriggered = guestTimesTriggered + ?
					WHERE	notificationID = ?";
				$statement = WCF::getDB()->prepareStatement($sql);
				
				WCF::getDB()->beginTransaction();
				foreach ($notificationIDs as $notificationID) {
					$statement->execute([
						1,
						$notificationObject->getAuthorID() ? 0 : 1,
						$notificationID
					]);
				}
				WCF::getDB()->commitTransaction();
				
				$triggerCountParameters = $parameters;
				$triggerCountParameters['updateTriggerCount'] = $notificationIDs;
				EventHandler::getInstance()->fireAction($this, 'updateTriggerCount', $parameters);
				unset($triggerCountParameters);
			}
		}
		
		$recipientIDs = array_diff($recipientIDs, array_keys($notifications));
		if (empty($recipientIDs)) {
			return;
		}
		
		// remove recipients that are blocking the current user
		if ($userProfile->getUserID()) {
			// we use a live query here to avoid offloading this to the UserProfile
			// class, as we're potentially dealing with a lot of users and loading
			// their user storage data can get really expensive
			$conditions = new PreparedStatementConditionBuilder();
			$conditions->add("userID IN (?)", [$recipientIDs]);
			$conditions->add("ignoreUserID = ?", [$userProfile->getUserID()]);
			
			$sql = "SELECT  userID
				FROM    wcf" . WCF_N . "_user_ignore
				".$conditions;
			$statement = WCF::getDB()->prepareStatement($sql);
			$statement->execute($conditions->getParameters());
			$userIDs = [];
			while ($userID = $statement->fetchColumn()) {
				$userIDs[] = $userID;
			}
			<commit_msg>Fix parameter in `updateTriggerCount` event<commit_after><?php
namespace wcf\system\user\notification;
use wcf\data\object\type\ObjectType;
use wcf\data\object\type\ObjectTypeCache;
use wcf\data\user\notification\event\recipient\UserNotificationEventRecipientList;
use wcf\data\user\notification\event\UserNotificationEventList;
use wcf\data\user\notification\UserNotification;
use wcf\data\user\notification\UserNotificationAction;
use wcf\data\user\User;
use wcf\data\user\UserEditor;
use wcf\data\user\UserProfile;
use wcf\system\cache\builder\UserNotificationEventCacheBuilder;
use wcf\system\cache\runtime\UserProfileRuntimeCache;
use wcf\system\database\util\PreparedStatementConditionBuilder;
use wcf\system\email\mime\MimePartFacade;
use wcf\system\email\mime\RecipientAwareTextMimePart;
use wcf\system\email\Email;
use wcf\system\email\UserMailbox;
use wcf\system\event\EventHandler;
use wcf\system\exception\SystemException;
use wcf\system\request\LinkHandler;
use wcf\system\user\notification\event\IUserNotificationEvent;
use wcf\system\user\notification\object\type\IUserNotificationObjectType;
use wcf\system\user\notification\object\IUserNotificationObject;
use wcf\system\user\storage\UserStorageHandler;
use wcf\system\SingletonFactory;
use wcf\system\WCF;

/**
 * Handles user notifications.
 * 
 * @author	<NAME>, <NAME>
 * @copyright	2001-2019 WoltLab GmbH, <NAME>
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @package	WoltLabSuite\Core\System\User\Notification
 */
class UserNotificationHandler extends SingletonFactory {
	/**
	 * list of available object types
	 * @var	IUserNotificationObjectType[]
	 */
	protected $availableObjectTypes = [];
	
	/**
	 * list of available events
	 * @var	IUserNotificationEvent[][]
	 */
	protected $availableEvents = [];
	
	/**
	 * number of outstanding notifications
	 * @var	integer
	 */
	protected $notificationCount = null;
	
	/**
	 * list of object types
	 * @var	ObjectType[]
	 */
	protected $objectTypes = [];
	
	/**
	 * @inheritDoc
	 */
	protected function init() {
		// get available object types
		$this->objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.notification.objectType');
		foreach ($this->objectTypes as $typeName => $object) {
			$this->availableObjectTypes[$typeName] = $object->getProcessor();
		}
		
		// get available events
		$this->availableEvents = UserNotificationEventCacheBuilder::getInstance()->getData();
	}
	
	/**
	 * Triggers a notification event.
	 * 
	 * @param	string				$eventName
	 * @param	string				$objectType
	 * @param	IUserNotificationObject		$notificationObject
	 * @param	integer[]			$recipientIDs
	 * @param	mixed[]				$additionalData
	 * @param	integer				$baseObjectID
	 * @throws	SystemException
	 */
	public function fireEvent($eventName, $objectType, IUserNotificationObject $notificationObject, array $recipientIDs, array $additionalData = [], $baseObjectID = 0) {
		// check given object type and event name
		if (!isset($this->availableEvents[$objectType][$eventName])) {
			throw new SystemException("Unknown event ".$objectType."-".$eventName." given");
		}
		
		// get objects
		$objectTypeObject = $this->availableObjectTypes[$objectType];
		$event = $this->availableEvents[$objectType][$eventName];
		
		// get author's profile
		$userProfile = null;
		if ($notificationObject->getAuthorID()) {
			if ($notificationObject->getAuthorID() == WCF::getUser()->userID) {
				$userProfile = new UserProfile(WCF::getUser());
			}
			else {
				$userProfile = UserProfileRuntimeCache::getInstance()->getObject($notificationObject->getAuthorID());
			}
		}
		if ($userProfile === null) {
			$userProfile = new UserProfile(new User(null, []));
		}
		
		// set object data
		$event->setObject(new UserNotification(null, []), $notificationObject, $userProfile, $additionalData);
		$event->setAuthors([$event->getAuthorID() => $event->getAuthor()]);
		
		$parameters = [
			'eventName' => $eventName,
			'objectType' => $objectType,
			'notificationObject' => $notificationObject,
			'recipientIDs' => $recipientIDs,
			'additionalData' => $additionalData,
			'baseObjectID' => $baseObjectID,
			'objectTypeObject' => $objectTypeObject,
			'userProfile' => $userProfile,
			'event' => $event
		];
		// @deprecated 5.2 This event exposes incomplete data and should not be used, please use the following events instead.
		EventHandler::getInstance()->fireAction($this, 'fireEvent', $parameters);
		
		// find existing notifications
		$conditions = new PreparedStatementConditionBuilder();
		$conditions->add("userID IN (?)", [$recipientIDs]);
		$conditions->add("eventID = ?", [$event->eventID]);
		$conditions->add("eventHash = ?", [$event->getEventHash()]);
		$conditions->add("confirmTime = ?", [0]);
		
		$sql = "SELECT	notificationID, userID
			FROM	wcf".WCF_N."_user_notification
			".$conditions;
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute($conditions->getParameters());
		$notifications = $statement->fetchMap('userID', 'notificationID');
		
		// check if event supports stacking and author should be added
		if (!empty($notifications) && $event->isStackable()) {
			$conditions = new PreparedStatementConditionBuilder();
			$conditions->add("notificationID IN (?)", [array_values($notifications)]);
			if ($notificationObject->getAuthorID()) {
				$conditions->add("authorID = ?", [$notificationObject->getAuthorID()]);
			}
			else {
				$conditions->add("authorID IS NULL");
			}
			
			$sql = "SELECT	notificationID
				FROM	wcf".WCF_N."_user_notification_author
				".$conditions;
			$statement = WCF::getDB()->prepareStatement($sql);
			$statement->execute($conditions->getParameters());
			$notificationIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
			
			// filter array of existing notifications and remove values which
			// do not have a notification from this author yet (inverse logic!)
			foreach ($notifications as $userID => $notificationID) {
				if (!in_array($notificationID, $notificationIDs)) {
					unset($notifications[$userID]);
				}
			}
			
			if (!empty($notificationIDs)) {	
				// update trigger count
				$sql = "UPDATE	wcf".WCF_N."_user_notification
					SET	timesTriggered = timesTriggered + ?,
						guestTimesTriggered = guestTimesTriggered + ?
					WHERE	notificationID = ?";
				$statement = WCF::getDB()->prepareStatement($sql);
				
				WCF::getDB()->beginTransaction();
				foreach ($notificationIDs as $notificationID) {
					$statement->execute([
						1,
						$notificationObject->getAuthorID() ? 0 : 1,
						$notificationID
					]);
				}
				WCF::getDB()->commitTransaction();
				
				$triggerCountParameters = $parameters;
				$triggerCountParameters['updateTriggerCount'] = $notificationIDs;
				EventHandler::getInstance()->fireAction($this, 'updateTriggerCount', $triggerCountParameters);
				unset($triggerCountParameters);
			}
		}
		
		$recipientIDs = array_diff($recipientIDs, array_keys($notifications));
		if (empty($recipientIDs)) {
			return;
		}
		
		// remove recipients that are blocking the current user
		if ($userProfile->getUserID()) {
			// we use a live query here to avoid offloading this to the UserProfile
			// class, as we're potentially dealing with a lot of users and loading
			// their user storage data can get really expensive
			$conditions = new PreparedStatementConditionBuilder();
			$conditions->add("userID IN (?)", [$recipientIDs]);
			$conditions->add("ignoreUserID = ?", [$userProfile->getUserID()]);
			
			$sql = "SELECT  userID
				FROM    wcf" . WCF_N . "_user_ignore
				".$conditions;
			$statement = WCF::getDB()->prepareStatement($sql);
			$statement->execute($conditions->getParameters());
			$userIDs = [];
			while ($userID = $statement->fetchColumn()) {
				$userIDs[] = $userID;
			}
			
{'fraction_non_alphanumeric': 0.14302206259620318, 'fraction_numerical': 0.0019240636223704464, 'mean_word_length': 4.551085795656817, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 143, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2019871', 'n_tokens_mistral': 5269, 'n_tokens_neox': 4499, 'n_words': 942}