<?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/>.// Read and count commentsclass CommentFiles extends DataFiles{	protected $setup;	protected $thread;	public function __construct (Setup $setup, Thread $thread)	{		// Construct parent class		parent::__construct ($setup);		// Store parameters as properties		$this->setup = $setup;		$this->thread = $thread;	}	// Gets the appropriate thread directory path	protected function getThreadRoot ($thread)	{		// Use thread directory path if told to		if ($thread !== 'auto') {			return $this->setup->joinPaths ($this->setup->threadsPath, $thread);		}		// Otherwise, use thread from setup		return $this->setup->threadPath;	}	// Returns a comment file path for a given file and thread	protected function getCommentPath ($file, $extension, $thread = 'auto')	{		// Get thread root		$thread = $this->getThreadRoot ($thread);		// Construct full file path		$path = $this->setup->joinPaths ($thread, $file . '.' . $extension);		return $path;	}	// Read directory contents, put filenames in array, count files	protected function loadFiles ($extension)	{		// Initial comments to return		$comments = array ();		// File pattern to match against		$pattern = $this->setup->threadPath . '/*.' . $extension;		// Find files matching the pattern, with no sorting		$files = glob ($pattern, GLOB_NOSORT);		// Run through comments		foreach ($files as $file) {			// Get file name without extension			$key = basename ($file, '.' . $extension);			// Add file name to comments			$comments[$key] = (string)($key);		}		return $comments;	}	// Gets the appropriate metadata directory path	protected function getMetaDirectory ($thread, $global)	{		// Check if we're getting metadata for a specific thread		if ($global !== true) {			// If so, use the thread's path			$directory = $this->getThreadRoot ($thread) . '/metadata';		} else {			// If not, use the global metadata path			$directory = $this->setup->commentsPath . '/metadata';		}		// Return metadata directory path		return $directory;	}	// Gets the appropriate metadata file path	protected function getMetaPath ($name, $thread, $global)	{		// Metadata directory path		$directory = $this->getMetaDirectory ($thread, $global);		// Metadata file path		$path = $this->setup->joinPaths ($directory, $name . '.json');		// Return metadata file path		return $path;	}	// Creates metadata directory if it doesn't exist	protected function setupMeta ($thread, $global)	{		// Metadata directory path		$metadata = $this->getMetaDirectory ($thread, $global);		// Check if metadata root directory exists		if (file_exists ($this->getThreadRoot ($thread))) {			// If so, attempt to create metadata directory			if (file_exists ($metadata) or @mkdir ($metadata, 0755, true)) {				// If successful, set permissions to 0755 again				@chmod ($metadata, 0755);				return true;			}			// Otherwise throw exception			throw new \Exception (sprintf (				'Failed to create metadata directory at: %s',				$metadata			));		}	}	// Gets and returns latest comments from metadata files	protected function getLatestComments ($path, $thread, $global)	{		// Latest comments		$comments = array ();		// Attempt to read metadata		$latest = $this->readJSON ($path);		// Do nothing if JSON could not be read		if ($latest === false) {			return $comments;		}		// Run through the latest comments		foreach ($latest as $item) {			// Get comment key			$key = basename ($item);			// Decide proper thread			$thread = $global ? dirname ($item) : $thread;			// Initial comment data			$data = array ('comment' => $key, 'thread' => $thread);			// Attempt to read comment			$comment = $this->thread->data->read ($key, $thread);			// Check if comment read successfully			if ($comment !== false) {				// If so, get comment status				$status = Misc::getArrayItem ($comment, 'status') ?: 'approved';				// Add comment to array if it is approved				if ($status === 'approved') {					$comments[] = array_merge ($data, $comment);				}			}		}		return $comments;	}	// Read and return specific metadata from JSON file	public function readMeta ($name, $thread = 'auto', $global = false)	{		// Metadata JSON file path		$path = $this->getMetaPath ($name, $thread, $global);		// Choose statement for supported metadata		switch ($name) {			// Latest comments			case 'latest-comments': {				return $this->getLatestComments ($path, $thread, $global);			}			// All others, just try to read as-is			default: {				return $this->readJSON ($path);			}		}	}	// Save metadata to specific metadata JSON file	public function saveMeta ($name, array $data, $thread = 'auto', $global = false)	{		// Metadata JSON file path		$metadata_path = $this->getMetaPath ($name, $thread, $global);		// Create metadata directory if it doesn't exist		$this->setupMeta ($thread, $global);		// Check if metadata root directory exists		if (file_exists ($this->getThreadRoot ($thread))) {			// If so, attempt to save data to metadata JSON file			if ($this->saveJSON ($metadata_path, $data) === false) {				// Throw exception on failure				throw new \Exception (					'Failed to save metadata!'				);			}		}		return true;	}	// Check if comment thread directory exists	public function checkThread ()	{		// Attempt to create the directory		if (!file_exists ($this->setup->threadPath)		    and !@mkdir ($this->setup->threadPath, 0755, true)		    and !@chmod ($this->setup->threadPath, 0755))		{			throw new \Exception (sprintf (				'Failed to create comment thread directory at: %s',				$this->setup->threadPath			));		}		// If yes, check if it is or can be made to be writable		if (!is_writable ($this->setup->threadPath)		    and !@chmod ($this->setup->threadPath, 0755))		{			throw new \Exception (sprintf (				'Comment thread directory at %s is not writable.',				$this->setup->threadPath			));		}	}	// Queries an array of directory names	protected function queryDirs ($path, $callback = false)	{		// Directory names output		$names = array ();		// Get comment directories		$dirs = glob ($path . '/*', GLOB_ONLYDIR);		// Convert directories paths to just their names		foreach ($dirs as $name) {			// Check callback conditions			$match = $callback ? $callback ($name) : true;			// And add directory if callback returns true			if ($match === true) {				$names[] = basename ($name);			}		}		return $names;	}	// Queries an array of websites	public function queryWebsites ()	{		// Path to get website directories from		$path = $this->setup->commentsRoot;		// Get website directories		$websites = $this->queryDirs ($path, function ($name) {			return file_exists ($name . '/threads');		});		return $websites;	}	// Queries an array of comment threads	public function queryThreads ()	{		return $this->queryDirs ($this->setup->threadsPath);	}	// Prepends a comment to latest comments metadata file	protected function prependLatestComments ($file, $global = false)	{		// Add thread to file if metadata is global		if ($global === true) {			$file = $this->setup->joinPaths ($this->setup->threadName, $file);		}		// Initial latest comments metadata array		$latest = array ($file);		// Get metadata file path		$path = $this->getMetaPath ('latest-comments', 'auto', $global);		// Attempt to read existing latest comments metadata		$metadata = $this->readJSON ($path);		// Merge existing comments with initial array		if ($metadata !== false) {			$latest = array_merge ($latest, $metadata);		}		// Maximum number of latest comments to store		$latest_max = max (10, $this->setup->latestMax);		// Limit latest comments metadata array to configurable size		$latest = array_slice ($latest, 0, $latest_max);		// Attempt to save latest comments metadata		$this->saveMeta ('latest-comments', $latest, 'auto', $global);	}	// Removes a comment from latest comments metadata file	protected function spliceLatestComments ($file, $global = false)	{		// Add thread to file if metadata is global		if ($global === true) {			$file = $this->setup->joinPaths ($this->setup->threadName, $file);		}		// Get metadata file path		$path = $this->getMetaPath ('latest-comments', 'auto', $global);		// Attempt to read existing latest comments metadata		$latest = $this->readJSON ($path);		// Check if latest comments metadata read successfully		if ($latest !== false) {			// If so, get index of file in array			$index = array_search ($file, $latest);			// Remove comment from latest array			if ($index !== false) {				array_splice ($latest, $index, 1);			}			// Attempt to save latest comments metadata			$this->saveMeta ('latest-comments', $latest, 'auto', $global);		}	}	// Adds a comment to latest comments metadata file	public function addLatestComment ($file)	{		// Add comment to thread-specific latest comments metadata		$this->prependLatestComments ($file);		// Add comment to global latest comments metadata		$this->prependLatestComments ($file, true);	}	// Removes a comment from latest comments metadata file	public function removeFromLatest ($file)	{		// Add comment to thread-specific latest comments metadata		$this->spliceLatestComments ($file);		// Add comment to global latest comments metadata		$this->spliceLatestComments ($file, true);	}}