<?php namespace HashOver;// Copyright (C) 2010-2021 Jacob Barkdull// This file is part of HashOver.//// HashOver is free software: you can redistribute it and/or modify// it under the terms of the GNU Affero General Public License as// published by the Free Software Foundation, either version 3 of the// License, or (at your option) any later version.//// HashOver 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 Affero General Public License for more details.//// You should have received a copy of the GNU Affero General Public License// along with HashOver.  If not, see <http://www.gnu.org/licenses/>.class PHPMode{	protected $setup;	protected $ui;	protected $comments;	protected $rawComments;	protected $crypto;	protected $locale;	protected $templater;	protected $markdown;	protected $trimTagRegexes = array (		'blockquote' => '/(<blockquote>)([\s\S]*?)(<\/blockquote>)/iS',		'ul' => '/(<ul>)([\s\S]*?)(<\/ul>)/iS',		'ol' => '/(<ol>)([\s\S]*?)(<\/ol>)/iS'	);	protected $linkRegex = '/((http|https|ftp):\/\/[a-z0-9-@:;%_\+.~#?&\/=]+) {0,1}/iS';	protected $codeTagCount = 0;	protected $codeTags = array ();	protected $preTagCount = 0;	protected $preTags = array ();	protected $paragraphRegex = '/(?:\r\n|\r|\n){2}/S';	protected $lineRegex = '/(?:\r\n|\r|\n)/S';	public function __construct (Setup $setup, CommentsUI $ui, array $comments, array $raw)	{		// Store parameters as properties		$this->setup = $setup;		$this->ui = $ui;		$this->comments = $comments;		$this->rawComments = $raw;		// Instantiate various classes		$this->crypto = new Crypto ();		$this->locale = new Locale ($setup);		$this->templater = new Templater ($setup);		$this->markdown = new Markdown ();	}	protected function fileFromPermalink ($permalink)	{		$file = substr ($permalink, 1);		$file = str_replace ('r', '-', $file);		$file = str_replace ('-pop', '', $file);		return $file;	}	protected function replyCheck ($permalink)	{		if (empty ($_GET['hashover-reply'])) {			return;		}		if ($_GET['hashover-reply'] === $permalink) {			$file = $this->fileFromPermalink ($permalink);			$form = new HTMLTag ('form', array (				'id' => $this->ui->prefix ('reply-' . $permalink),				'class' => 'hashover-reply-form',				'method' => 'post',				'action' => $this->setup->getBackendPath ('form-actions.php')			));			$subscribed = ($this->setup->subscribesUser === true);			$form->innerHTML ($this->ui->replyForm ($permalink, $this->setup->pageURL, $this->setup->threadName, $this->setup->pageTitle, $file, $subscribed));			return $form->asHTML ();		}	}	protected function editCheck ($comment)	{		if (empty ($_GET['hashover-edit'])) {			return;		}		$permalink = Misc::getArrayItem ($comment, 'permalink') ?: '';		if ($_GET['hashover-edit'] === $permalink) {			$file = $this->fileFromPermalink ($permalink);			$body = $comment['body'];			$body = preg_replace ($this->linkRegex, '\\1', $body);			$status = Misc::getArrayItem ($comment, 'status') ?: 'approved';			$name = Misc::getArrayItem ($comment, 'name') ?: '';			$website = Misc::getArrayItem ($comment, 'website') ?: '';			$subscribed = isset ($comment['subscribed']);			if (!empty ($this->rawComments[$file])) {				$raw_comment = $this->rawComments[$file];				$email = Misc::getArrayItem ($raw_comment, 'email') ?: '';				$encryption = Misc::getArrayItem ($raw_comment, 'encryption') ?: '';				$email = $this->crypto->decrypt ($email, $encryption);			} else {				$email = '';			}			$form = new HTMLTag ('form', array (				'id' => $this->ui->prefix ('edit-' . $permalink),				'class' => 'hashover-edit-form',				'method' => 'post',				'action' => $this->setup->getBackendPath ('form-actions.php')			), false);			$edit_form = $this->ui->editForm ($permalink, $this->setup->pageURL, $this->setup->threadName, $this->setup->pageTitle, $file, $name, $email, $website, $body, $status, $subscribed);			$form->innerHTML ($edit_form);			return $form->asHTML ();		}	}	protected function codeTagReplace ($grp)	{		$code_placeholder = $grp[1] . 'CODE_TAG[' . $this->codeTagCount . ']' . $grp[3];		$this->codeTags[$this->codeTagCount] = trim ($grp[2], "\r\n");		$this->codeTagCount++;		return $code_placeholder;	}	protected function codeTagReturn ($grp)	{		return $this->codeTags[($grp[1])];	}	protected function preTagReplace ($grp)	{		$pre_placeholder = $grp[1] . 'PRE_TAG[' . $this->preTagCount . ']' . $grp[3];		$this->preTags[$this->preTagCount] = trim ($grp[2], "\r\n");		$this->preTagCount++;		return $pre_placeholder;	}	protected function preTagReturn ($grp)	{		return $this->preTags[($grp[1])];	}	// Returns the permalink of a comment's parent	protected function getParentPermalink ($permalink)	{		$permalink_parts = explode ('r', $permalink);		array_pop ($permalink_parts);		return implode ('r', $permalink_parts);	}	// Find a comment by its permalink	protected function findByPermalink ($permalink, $comments)	{		// Loop through all comments		foreach ($comments as $comment) {			// Return comment if its permalink matches			if ($comment['permalink'] === $permalink) {				return $comment;			}			// Recursively check replies when present			if (!empty ($comment['replies'])) {				$reply = $this->findByPermalink ($permalink, $comment['replies']);				if ($reply !== '') {					return $reply;				}			}		}		// Otherwise, return nothing		return '';	}	public function parseComment (array $comment, $parent = '', $popular = false)	{		$permalink = $comment['permalink'];		$first_instance = 'hashover-' . $permalink;		$name_class = 'hashover-name-plain';		$this->codeTagCount = 0;		$this->codeTags = array ();		$this->preTagCount = 0;		$this->preTags = array ();		// Get instantiated prefix		$prefix = $this->ui->prefix ();		// Initial template		$template = array (			'hashover' => $prefix,			'permalink' => $permalink		);		// Text for avatar image alt attribute		$permatext = substr ($permalink, 1);		$permatext = explode ('r', $permatext);		$permatext = array_pop ($permatext);		// Wrapper element for each comment		$comment_wrapper = $this->ui->commentWrapper ($permalink);		// Check if this comment is a popular comment		if ($popular === true) {			// Attempt to get parent comment permalink			$parent = $this->getParentPermalink ($permalink);			// Get parent comment by its permalink if it exists			if ($parent !== '') {				$parent = $this->findByPermalink ($parent, $this->comments['primary']);			}			// And remove "-pop" from text for avatar			$permatext = str_replace ('-pop', '', $permatext);		} else {			// Append class to indicate comment is a reply when appropriate			if ($parent !== '') {				$comment_wrapper->appendAttribute ('class', 'hashover-reply');			}		}		// Add avatar image to template		$template['avatar'] = $this->ui->userAvatar ($comment['avatar'], $permalink, $permatext);		if (!isset ($comment['notice'])) {			$name = Misc::getArrayItem ($comment, 'name') ?: $this->setup->defaultName;			$is_twitter = false;			// Check if user's name is a Twitter handle			if ($name[0] === '@') {				$name = mb_substr ($name, 1);				$name_class = 'hashover-name-twitter';				$is_twitter = true;				$name_length = mb_strlen ($name);				// Check if Twitter handle is valid length				if ($name_length > 1 and $name_length <= 30) {					// Set website to Twitter profile if a specific website wasn't given					if (empty ($comment['website'])) {						$comment['website'] = 'https://twitter.com/' . $name;					}				}			}			// Check whether user gave a website			if (!empty ($comment['website'])) {				if ($is_twitter === false) {					$name_class = 'hashover-name-website';				}				// If so, display name as a hyperlink				$name_link = $this->ui->nameElement ('a', $permalink, $name, $comment['website']);			} else {				// If not, display name as plain text				$name_link = $this->ui->nameElement ('span', $permalink, $name);			}			// Check if comment has a parent			if ($parent !== '') {				// If so, create the parent thread permalink				$parent_thread = 'hashover-' . $parent['permalink'];				// Get the parent's name				$parent_name = Misc::getArrayItem ($parent, 'name') ?: $this->setup->defaultName;				// Add thread parent hyperlink to template				$template['parent-link'] = $this->ui->parentThreadLink ($this->setup->filePath, $parent_thread, $permalink, $parent_name);			}			if (isset ($comment['user-owned'])) {				// Append class to indicate comment is from logged in user				$comment_wrapper->appendAttribute ('class', 'hashover-user-owned');				// Define "Reply" link with original poster title				$reply_title = $this->locale->text['commenter-tip'];				$reply_class = 'hashover-no-email';			} else {				// Check if commenter is subscribed				if (isset ($comment['subscribed'])) {					// If so, set subscribed title					$reply_title = $name . ' ' . $this->locale->text['subscribed-tip'];					$reply_class = 'hashover-has-email';				} else{					// If not, set unsubscribed title					$reply_title = $name . ' ' . $this->locale->text['unsubscribed-tip'];					$reply_class = 'hashover-no-email';				}			}			// Check if the comment is editable for the user			if (isset ($comment['editable'])) {				// If so, add "Edit" hyperlink to template				if (!empty ($_GET['hashover-edit']) and $_GET['hashover-edit'] === $permalink) {					$template['edit-link'] = $this->ui->cancelLink ($first_instance, 'edit');				} else {					$template['edit-link'] = $this->ui->formLink ($this->setup->filePath, 'edit', $permalink);				}			}			// Check if the comment has been liked			if (isset ($comment['likes'])) {				// Add likes to HTML template				$template['likes'] = $comment['likes'];				// Check if there is more than one like				if ($comment['likes'] !== 1) {					// If so, use "X Likes" locale					$like_count = $comment['likes'] . ' ' . $this->locale->text['likes'];				} else {					// If not, use "X Like" locale					$like_count = $comment['likes'] . ' ' . $this->locale->text['like'];				}				// Add like count to HTML template				$template['like-count'] = $this->ui->likeCount ('likes', $permalink, $like_count);			}			// Check if dislikes are enabled and the comment's been disliked			if ($this->setup->allowsDislikes === true			    and isset ($comment['dislikes']))			{				// Add likes to HTML template				$template['dislikes'] = $comment['dislikes'];				// Check if there is more than one dislike				if ($comment['dislikes'] !== 1) {					// If so, use "X Dislikes" locale					$dislike_count = $comment['dislikes'] . ' ' . $this->locale->text['dislikes'];				} else {					// If not, use "X Dislike" locale					$dislike_count = $comment['dislikes'] . ' ' . $this->locale->text['dislike'];				}				// Add dislike count to HTML template				$template['dislike-count'] = $this->ui->likeCount ('dislikes', $permalink, $dislike_count);			}			// Add name HTML to template			$template['name'] = $this->ui->nameWrapper ($name_class, $name_link);			// Add IP address HTML to template			if (!empty ($comment['ipaddr'])) {				$template['ipaddr'] = $this->ui->ipWrapper ($comment['ipaddr']);			}			// Append status text to date			if (!empty ($comment['status-text'])) {				$comment['date'] .= ' (' . $comment['status-text'] . ')';			}			// Add date permalink hyperlink to template			$template['date'] = $this->ui->dateLink ($this->setup->filePath, $first_instance, $comment['date-time'], $comment['date']);			// Add "Reply" hyperlink to template			if (!empty ($_GET['hashover-reply']) and $_GET['hashover-reply'] === $permalink) {				$template['reply-link'] = $this->ui->cancelLink ($first_instance, 'reply', $reply_class);			} else {				$template['reply-link'] = $this->ui->formLink ($this->setup->filePath, 'reply', $permalink, $reply_class, $reply_title);			}			// Add edit form HTML to template			if (isset ($comment['editable'])) {				$template['edit-form'] = $this->editCheck ($comment);			}			// Add reply form HTML to template			$template['reply-form'] = $this->replyCheck ($permalink);			// Add reply count to template			if (!empty ($comment['replies'])) {				$template['reply-count'] = count ($comment['replies']);				if ($template['reply-count'] > 0) {					if ($template['reply-count'] !== 1) {						$template['reply-count'] .= ' ' . $this->locale->text['replies'];					} else {						$template['reply-count'] .= ' ' . $this->locale->text['reply'];					}				}			}			// Add comment data to template			$template['comment'] = $comment['body'];			// Remove [img] tags			$template['comment'] = preg_replace ('/\[(img|\/img)\]/iS', '', $template['comment']);			// Add HTML anchor tag to URLs (hyperlinks)			$template['comment'] = preg_replace ($this->linkRegex, '<a href="\\1" rel="noopener noreferrer" target="_blank">\\1</a>', $template['comment']);			// Parse markdown in comment			if ($this->setup->usesMarkdown !== false) {				$template['comment'] = $this->markdown->parseMarkdown ($template['comment']);			}			// Replace code tags with placeholder text			if (mb_strpos ($template['comment'], '<code>') !== false) {				$template['comment'] = preg_replace_callback ('/(<code>)([\s\S]*?)(<\/code>)/iS', 'self::codeTagReplace', $template['comment']);			}			// Replace pre tags with placeholder text			if (mb_strpos ($template['comment'], '<pre>') !== false) {				$template['comment'] = preg_replace_callback ('/(<pre>)([\s\S]*?)(<\/pre>)/iS', 'self::preTagReplace', $template['comment']);			}			// Check for various multi-line tags			foreach ($this->trimTagRegexes as $tag => $trimTagRegex) {				if (mb_strpos ($template['comment'], '<' . $tag . '>') !== false) {					// Trim leading and trailing whitespace					$template['comment'] = preg_replace_callback ($trimTagRegex, function ($grp) {						return $grp[1] . trim ($grp[2], "\r\n") . $grp[3];					}, $template['comment']);				}			}			// Break comment into paragraphs			$paragraphs = preg_split ($this->paragraphRegex, $template['comment']);			$pd_comment = '';			// Wrap each paragraph in <p> tags and place <br> tags after each line			for ($i = 0, $il = count ($paragraphs); $i < $il; $i++) {				$pd_comment .= '<p>' . preg_replace ($this->lineRegex, '<br>', $paragraphs[$i]) . '</p>' . PHP_EOL;			}			// Replace code tag placeholders with original code tag HTML			if ($this->codeTagCount > 0) {				$pd_comment = preg_replace_callback ('/CODE_TAG\[([0-9]+)\]/S', 'self::codeTagReturn', $pd_comment);			}			// Replace pre tag placeholders with original pre tag HTML			if ($this->preTagCount > 0) {				$pd_comment = preg_replace_callback ('/PRE_TAG\[([0-9]+)\]/S', 'self::preTagReturn', $pd_comment);			}			// Add paragraph'd comment data to template			$template['comment'] = $pd_comment;		} else {			// Append notice class			$comment_wrapper->appendAttribute ('class', 'hashover-notice');			$comment_wrapper->appendAttribute ('class', $comment['notice-class']);			// Add notice to template			$template['comment'] = $comment['notice'];			// Set name to 'Comment Deleted!'			$template['name'] = $this->ui->nameWrapper ($name_class, $comment['title']);		}		// Parse theme layout HTML template		$theme_html = $this->templater->parseTheme ('comments.html', $template);		// Comment HTML template		$comment_wrapper->innerHTML ($theme_html);		// Check if comment has replies		if (!empty ($comment['replies'])) {			// If so, append class to indicate comment has replies			$comment_wrapper->appendAttribute ('class', 'hashover-has-replies');			// Recursively parse replies			foreach ($comment['replies'] as $reply) {				$comment_wrapper->appendInnerHTML ($this->parseComment ($reply, $comment));			}		}		return $comment_wrapper->asHTML ();	}}