<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Finder extends MY_Controller
{
	private $dirs = array();
	private $data = array("show_menu" => true);

	public function __construct()
	{
		parent::__construct();

		$this->admin_login_required();
		$this->current_user = get_user();
		$this->data["user"] = $this->current_user;
	}

	public function index()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		$this->load->model(array("category", "document"));
		$ck = ($this->input->get("CKEditorFuncNum") != "");

		$this->data["show_menu"] 			= !$ck;
		$this->data["type"] 			 	= $this->input->get("type");
		$this->data["CKEditorFuncNum"]  	= $this->input->get("CKEditorFuncNum");

		$sites 		= $this->site->get();
		$categories = $this->category->get();
		$site_id 	= Mainframe::active_site_id();
		$files 		= $this->document->getFiles($site_id, $this->current_user);

		$this->data["files"] 		= $files;
		$this->data["site_id"] 		= $site_id;
		$this->data["sites"] 		= $sites;
		$this->data["categories"]	= $categories;

		$this->load->view("common/header", $this->data);
		$this->load->view("common/message", array("messages" => $this->messages, "errors" => $this->errors));
		$this->load->view("finder/finder", $this->data);
		$this->load->view("finder/upload", $this->data);
		$this->load->view("finder/resize", $this->data);
		$this->load->view("finder/metadata", $this->data);
		$this->load->view("finder/document", $this->data);
		$this->load->view("common/footer", $this->data);
	}

	public function upload()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		$filedata 	= $this->input->post("file");
		$break 		= strpos($filedata, ",");
		$headers 	= substr($filedata, 0, $break);
		$filedata 	= base64_decode(substr($filedata, $break+1));
		$path 		= ABSOLUTE_PATH . "/" . $this->input->post("cwd") . "/" . $this->strip_time($this->input->post("filename"));
		$retina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $path);
		$highdpi 	= $this->input->post("highdpi");
		$isimage 	= preg_match('/\.(jpe?g|png|gif|webp)$/i', $path);

		if(file_exists($path))
		{
			http_response_code(409);
			die("File Already Exists");
		}
		// Standard file upload, no retina copy created because we don't know what size it will be used at.
		else if((!$isimage || !$highdpi) && file_put_contents($path, $filedata))
		{
			http_response_code(200);
			$this->spit_json($this->get_filesystem("/" . $this->input->post("cwd")));
		}
		// Cropped images comes in at retina size, so we'll save that and then make a smaller copy from it.
		else if($isimage && $highdpi && file_put_contents($retina, $filedata))
		{
			// Create a retina copy automatically.
			$size = getimagesize($retina);
			createThumbnail($retina, $path, $size[0] / 2, $size[1] / 2, false, false, Mainframe::site()->webp ? IMAGETYPE_WEBP : null);

			http_response_code(200);
			$this->spit_json($this->get_filesystem("/" . $this->input->post("cwd")));
		}
		else
		{
			unlink($path);
			http_response_code(409);
			die("Could not upload file.");
		}
	}

	public function quick_upload()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		$this->load->model("document_log");
		$config = array();
		$user 	= get_user();

		if($this->input->get_post("type") == "images")
		{
			$config['upload_path'] 		= ABSOLUTE_PATH . '/images/';
			$config['allowed_types'] 	= 'gif|jpg|jpeg|png';
		}
		else
		{
			$config['upload_path'] 		= DOCUMENT_PATH . '/';
			$config['allowed_types'] 	= 'txt|md|csv|tsv|pdf|doc|docx|xls|xlsx|zip|gz|7z|odt|ai|svg|mp3|wav|mov|mp4|mpeg|mpg|log';
		}
		$config['encrypt_name']			= false;
		$config['remove_spaces']		= true;

		$this->load->library('upload', $config);

		if($this->upload->do_upload("upload"))
		{
			$path = str_replace(ABSOLUTE_PATH, "", $config['upload_path'] . $this->upload->file_name);

			if($this->input->get_post("type") != "images")
			{
				$this->load->model("document");
				$document 					= new Document();
				$document->site_id 			= Mainframe::active_site_id();
				$document->category_id 		= null;
				$document->title 			= $this->upload->file_name;
				$document->description 		= null;
				$document->filename 		= $this->upload->file_name;

				$document->url 				= preg_replace('/[^a-z0-9\-_]/', '-', strtolower($this->upload->file_name));
				$document->url 				= preg_replace('/-{2,10}/', '-', $document->url);
				$document->url 				= preg_replace('/(^-|-$)/', '', $document->url);

				$document->published 		= 1;
				$document->created 			= date("Y-m-d H:i:s");
				$document->created_user_id 	= $user->user_id;

				// Check if this URL is available, add numbers on the end if it isn't.
				$count 	= 0;
				$url 	= $document->url;
				do
				{
					$query = $this->db->query("SELECT `document_id` FROM `document` WHERE `document_id` !=? AND `url`=?", array($document->document_id, $url));

					if($query->num_rows() == 0)
					{
						break;
					}

					$count++;
					$url = $document->url . $count;
				}
				while(true);

				$document->url 	= $url;
				$document->save();

				$this->db->query("INSERT INTO `acl` SET `document_id`=?, `acl_role_id`=1 `read`=1, `write`=0", array($this->db->insert_id()));
				$this->db->query("INSERT INTO `acl` SET `document_id`=?, `acl_role_id`=2 `read`=1, `write`=0", array($this->db->insert_id()));
				$this->db->query("INSERT INTO `acl` SET `document_id`=?, `acl_role_id`=3 `read`=1, `write`=1", array($this->db->insert_id()));

				$this->document_log->logChanges(new Document(), $document, $user);
				$path = "/document/" . $document->url;
			}
			?>
			<script>
			window.parent.CKEDITOR.tools.callFunction(<?php echo($this->input->get("CKEditorFuncNum")); ?>, "<?php echo($path); ?>", "");
			</script>
			<?php
		}
		else
		{
			?>
			<script>
			alert("<?php echo($this->upload->error_msg[0]); ?>");
			window.parent.CKEDITOR.tools.callFunction(<?php echo($this->input->get("CKEditorFuncNum")); ?>, "", "");
			</script>
			<?php
		}
	}

	public function delete()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		if($this->input->post("path") == "")
		{
			http_response_code(409);
			die("Invalid Path");
		}

		$path = ABSOLUTE_PATH . $this->strip_time($this->input->post("path"));

		if(!file_exists($path))
		{
			http_response_code(409);
			die("Invalid Path");
		}
		else if(is_file($path))
		{
			$src_path = str_replace(ABSOLUTE_PATH . "/", "", $path);

			if($src_path != "")
			{
				$check1 = $this->db->query("SELECT COUNT(*) AS total, GROUP_CONCAT(p.`title` SEPARATOR '</li><li>') AS pages
				                           FROM `content_values` AS v
				                           INNER JOIN `pages` AS p USING(page_id)
				                           WHERE v.`value` LIKE '%" . $this->db->escape_str($src_path) . "%'");
				$check2 = $this->db->query("SELECT COUNT(*) AS total, GROUP_CONCAT(i.`tag` SEPARATOR '</li><li>') AS modules
				                           FROM `module_settings` AS s
				                           INNER JOIN `module_instances` AS i USING(module_instance_id)
				                           WHERE s.`value` LIKE '%" . $this->db->escape_str($src_path) . "%'");
				$row1 = $check1->row();
				$row2 = $check2->row();
				$total1 = $row1->total;
				$total2 = $row2->total;
				$total_links = $total1 + $total2;
				$link_details = "";

				if($total_links > 0)
				{
					if($total1 > 0)
					{
						$link_details .= "<br /><br />Pages with references:<ul><li>" . $row1->pages . "</li></ul>";
					}
					if($total2 > 0)
					{
						$link_details .= "<br /><br />Modules with references:<ul><li>" . $row2->modules . "</li></ul>";
					}

					http_response_code(409);
					die("You can not delete this file because " . $total_links . " active reference" . ($total_links == 1 ? " exists" : "s exist") . " on your website." . $link_details);
				}
			}

			if(unlink($path))
			{
				Mainframe::deleteThumbnails($path);

				// Automatically delete retina copies of images.
				$retina = preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $path);

				if(file_exists($retina))
				{
					unlink($retina);
					Mainframe::deleteThumbnails($retina);
				}

				// Automatically delete metadata file.
				$meta = $path . ".json";

				if(file_exists($meta))
				{
					unlink($meta);
					$this->cache->clean();
				}

				http_response_code(200);
				$this->spit_json($this->get_filesystem(dirname($this->input->post("path"))));
			}
			else
			{
				http_response_code(409);
				die("You don't have write access to this file." . $path);
			}
		}
		else if(is_dir($path))
		{
			if(rmdir($path))
			{
				http_response_code(200);
				die(str_replace(ABSOLUTE_PATH . "/", "", str_replace("/" . basename($path), "/", $path)));
			}
			else
			{
				http_response_code(409);
				die("You don't have filesystem access to this directory or the directory is not empty.");
			}
		}
	}

	public function rename()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		if($this->input->post("path") == "")
		{
			http_response_code(409);
			die("Invalid Path");
		}

		if($this->input->post("filename") == "")
		{
			http_response_code(409);
			die("Invalid Filename");
		}

		$path = ABSOLUTE_PATH . $this->strip_time($this->input->post("path"));

		if(!file_exists($path))
		{
			http_response_code(409);
			die("Invalid Path");
		}
		else if(is_file($path))
		{
			$newpath 	= dirname($path) . "/" . $this->input->post("filename");

			if(rename($path, $newpath))
			{
				// Silently update references to this file on pages or in modules.
				$old_url 	= str_replace(ABSOLUTE_PATH . "/", "", $path);
				$new_url 	= str_replace(ABSOLUTE_PATH . "/", "", $newpath);
				$this->db->query("UPDATE `content_values` SET `value`=REPLACE(`value`, " . $this->db->escape($old_url) . ", " . $this->db->escape($new_url) . ") WHERE `value` LIKE '%" . $this->db->escape_str($old_url) . "%'");
				$this->db->query("UPDATE `module_settings` SET `value`=REPLACE(`value`, " . $this->db->escape($old_url) . ", " . $this->db->escape($new_url) . ") WHERE `value` LIKE '%" . $this->db->escape_str($old_url) . "%'");

				// Automatically rename retina copies of images.
				$retina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $path);
				$newretina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $newpath);

				if(file_exists($retina))
				{
					rename($retina, $newretina);
				}

				// Automatically rename metadata file.
				$meta    = $path . ".json";
				$newmeta = $newpath . ".json";

				if(file_exists($meta))
				{
					rename($meta, $newmeta);
					$this->cache->clean();
				}

				http_response_code(200);
				$this->spit_json($this->get_filesystem(dirname($this->input->post("path"))));
			}
			else
			{
				http_response_code(409);
				die("You don't have write access to this file." . $path);
			}
		}
		else if(is_dir($path))
		{
			$newpath = str_replace("/" . basename($path), "/" . $this->input->post("filename"), $path);
			$src_path = str_replace(ABSOLUTE_PATH . "/", "", $path) . "/";

			if($src_path != "")
			{
				$check1 = $this->db->query("SELECT COUNT(*) AS total, GROUP_CONCAT(p.`title` SEPARATOR '</li><li>') AS pages
				                           FROM `content_values` AS v
				                           INNER JOIN `pages` AS p USING(page_id)
				                           WHERE v.`value` LIKE '%" . $this->db->escape_str($src_path) . "%'");
				$check2 = $this->db->query("SELECT COUNT(*) AS total, GROUP_CONCAT(i.`tag` SEPARATOR '</li><li>') AS modules
				                           FROM `module_settings` AS s
				                           INNER JOIN `module_instances` AS i USING(module_instance_id)
				                           WHERE s.`value` LIKE '%" . $this->db->escape_str($src_path) . "%'");
				$row1 = $check1->row();
				$row2 = $check2->row();
				$total1 = $row1->total;
				$total2 = $row2->total;
				$total_links = $total1 + $total2;
				$link_details = "";

				if($total_links > 0)
				{
					if($total1 > 0)
					{
						$link_details .= "<br /><br />Pages with references:<ul><li>" . $row1->pages . "</li></ul>";
					}
					if($total2 > 0)
					{
						$link_details .= "<br /><br />Modules with references:<ul><li>" . $row2->modules . "</li></ul>";
					}

					http_response_code(409);
					die("You can not rename this directory because " . $total_links . " active reference" . ($total_links == 1 ? " exists" : "s exist") . " on your website." . $link_details);
				}
			}

			if(rename($path, $newpath))
			{
				// Rename thumbnails directory too.
				@rename(str_replace("/images/", "/images/thumbnails/", $path), str_replace("/images/", "/images/thumbnails/", $newpath));

				// Update any module settings that were pointing to this directory.
				$this->db->query("UPDATE `module_settings` SET `value`=? WHERE `value`=?",
				                 array(str_replace(ABSOLUTE_PATH . "/", "", $newpath),
				                       str_replace(ABSOLUTE_PATH . "/", "", $path)));

				http_response_code(200);
				die(str_replace(ABSOLUTE_PATH . "/", "", $newpath));
			}
			else
			{
				http_response_code(409);
				die("You don't have filesystem permission to rename this directory.");
			}
		}
	}

	public function move()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		if($this->input->post("oldpath") == "" || $this->input->post("newpath") == "")
		{
			http_response_code(409);
			die("Invalid Path");
		}

		$oldpath = ABSOLUTE_PATH . $this->strip_time($this->input->post("oldpath"));
		$newpath = ABSOLUTE_PATH . $this->strip_time($this->input->post("newpath"));

		if(!file_exists($oldpath))
		{
			http_response_code(409);
			die("Invalid Path");
		}
		else if(file_exists($newpath))
		{
			http_response_code(409);
			die("File Already Exists");
		}
		else if(is_file($oldpath))
		{
			if(rename($oldpath, $newpath))
			{
				// Silently update references to this file on pages or in modules.
				$old_url 	= str_replace(ABSOLUTE_PATH . "/", "", $oldpath);
				$new_url 	= str_replace(ABSOLUTE_PATH . "/", "", $newpath);
				$this->db->query("UPDATE `content_values` SET `value`=REPLACE(`value`, " . $this->db->escape($old_url) . ", " . $this->db->escape($new_url) . ") WHERE `value` LIKE '%" . $this->db->escape_str($old_url) . "%'");
				$this->db->query("UPDATE `module_settings` SET `value`=REPLACE(`value`, " . $this->db->escape($old_url) . ", " . $this->db->escape($new_url) . ") WHERE `value` LIKE '%" . $this->db->escape_str($old_url) . "%'");

				// Automatically rename retina copies of images.
				$oldretina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $oldpath);
				$newretina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $newpath);

				if(file_exists($oldretina))
				{
					rename($oldretina, $newretina);
				}

				// Automatically rename metadata file.
				$meta    = $path . ".json";
				$newmeta = $newpath . ".json";

				if(file_exists($meta))
				{
					rename($meta, $newmeta);
					$this->cache->clean();
				}

				http_response_code(200);
				$this->spit_json($this->get_filesystem(dirname($this->input->post("oldpath"))));
			}
			else
			{
				http_response_code(409);
				die("You don't have filesystem write access to this directory." . $newpath);
			}
		}
		else if(is_dir($path))
		{
			http_response_code(409);
			die("You can't move a directory.");
		}
	}

	public function duplicate()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		if($this->input->post("path") == "")
		{
			http_response_code(409);
			die("Invalid Path");
		}

		$path = ABSOLUTE_PATH . $this->strip_time($this->input->post("path"));

		if(!file_exists($path))
		{
			http_response_code(409);
			die("Invalid Path");
		}
		else if(is_file($path))
		{
			$count = 0;
			$filename = basename($path);

			do
			{
				$count++;
				$newfile = dirname($path) . "/" . preg_replace('/\.(.*?)$/i', '-' . $count . '.$1', $filename);
			}
			while(file_exists($newfile));

			if(copy($path, $newfile))
			{
				// Automatically copy retina copies of images.
				$retina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $path);
				$newretina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $newfile);

				if(file_exists($retina))
				{
					copy($retina, $newretina);
				}

				// Automatically copy metadata file.
				$meta    = $path . ".json";
				$newmeta = $newfile . ".json";

				if(file_exists($meta))
				{
					copy($meta, $newmeta);
					$this->cache->clean();
				}

				http_response_code(200);
				$this->spit_json($this->get_filesystem(dirname($this->input->post("path"))));
			}
			else
			{
				http_response_code(409);
				die("You don't have write access to this file." . $path);
			}
		}
		else if(is_dir($path))
		{
			http_response_code(409);
			die("You can't duplicate a directory.");
		}
	}

	public function crop()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		$filedata 	= $this->input->post("image_data");
		$break 		= strpos($filedata, ",");
		$headers 	= substr($filedata, 0, $break);
		$filedata 	= base64_decode(substr($filedata, $break+1));
		$path 		= ABSOLUTE_PATH . $this->strip_time($this->input->post("filename"));
		$retina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $path);
		$highdpi 	= $this->input->post("highdpi");
		$duplicate  = $this->input->post("duplicate");

		if($duplicate)
		{
			$count = 0;
			$filename = basename($path);

			do
			{
				$count++;
				$newfile = dirname($path) . "/" . preg_replace('/\.(.*?)$/i', '-' . $count . '.$1', $filename);
			}
			while(file_exists($newfile));

			if(copy($path, $newfile))
			{
				// Automatically rename retina copies of images.
				$retina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $path);
				$newretina 	= preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $newfile);

				if(file_exists($retina))
				{
					copy($retina, $newretina);
				}

				$path 	= $newfile;
				$retina = $newretina;
			}
		}

		if(!file_exists($path))
		{
			http_response_code(409);
			die("File Doesn't Exist");
		}
		// The image comes in at retina size, so we'll save that and then make a smaller copy from it.
		else if(file_put_contents($retina, $filedata))
		{
			// Create a retina copy automatically.
			$size 	= getimagesize($retina);
			if(file_exists($path))
			{
				unlink($path);
			}
			Mainframe::deleteThumbnails($path);
			Mainframe::deleteThumbnails($retina);
			createThumbnail($retina, $path, $size[0] / 2, $size[1] / 2, false, false, Mainframe::site()->webp ? IMAGETYPE_WEBP : null);

			// If the user didn't want a high DPI copy, delete it.
			if(!$highdpi)
			{
				@unlink($retina);
			}

			http_response_code(200);
			$this->spit_json($this->get_filesystem(dirname($this->input->post("filename"))));
		}
		else
		{
			http_response_code(409);
			die("Could not crop file.");
		}
	}

	private function dirs($path)
	{
		static $count = 0;
		$html = '';

		if($path->type == "directory" &&
		   $path->path != "images/content" &&
		   $path->path != "images/shop" &&
		   $path->path != "images/thumbnails")
		{
			$count++;
			$skip = false;

			if(preg_match('/^documents/', $path->path) && !$this->current_user->has(ACTION_DOCUMENTS))
			{
				$skip = true;
			}

			if(!$skip)
			{
				$html .= '<li><em class="far fa-li fa-folder" aria-hidden="true"></em><a href="#" ondragover="nvfinder_DirDragHover(event)" ondragleave="nvfinder_DirDragUnhover(event)" ondrop="nvfinder_FileMoveHandler(event)" data-cwd="/' . $path->path . '" onclick="nvfinder_filter(\'' . $path->path . '\', event)" id="dir' . $count . '">' . basename($path->path) . '</a>';

				if(isset($path->files))
				{
					$html .= '<ul class="fa-ul">';

					foreach($path->files as $f)
					{
						$html .= $this->dirs($f);
					}

					$html .= '</ul>';
				}

				$html .= '</li>';
			}
		}

		return $html;
	}

	public function directories()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		echo($this->get_dirs());
	}

	public function filesystem()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		$this->spit_json($this->get_filesystem($this->input->post("path")));
	}

	private function get_filesystem($path)
	{
		if(!$path)
		{
			return array();
		}

		if(preg_match('/\/documents(\/)?/', $path))
		{
			return $this->documents(ABSOLUTE_PATH . $path);
		}
		else
		{
			return $this->files(ABSOLUTE_PATH . $path, true);
		}
	}

	private function get_dirs()
	{
		$directories = '';

		switch($this->input->get_post("type"))
		{
			case "images":
			{
				$x = new StdClass();
				$x->path = "images";
				$x->type = "directory";
				$x->files = $this->files(ABSOLUTE_PATH . "/images", false);
				$directories .= $this->dirs($x);

				break;
			}
			case "documents":
			{
				$x = new StdClass();
				$x->path = "documents";
				$x->type = "directory";
				$x->files = $this->documents(ABSOLUTE_PATH . "/documents");
				$directories .= $this->dirs($x);

				break;
			}
			default:
			{
				$x = new StdClass();
				$x->path = "images";
				$x->type = "directory";
				$x->files = $this->files(ABSOLUTE_PATH . "/images", false);
				$directories .= $this->dirs($x);

				$x = new StdClass();
				$x->path = "documents";
				$x->type = "directory";
				$x->files = $this->documents(ABSOLUTE_PATH . "/documents");
				$directories .= $this->dirs($x);

				$x = new StdClass();
				$x->path = "videos";
				$x->type = "directory";
				$x->files = $this->files(ABSOLUTE_PATH . "/videos", false);
				$directories .= $this->dirs($x);

				break;
			}
		}

		return $directories;
	}

	private function files($path, $generate_thumbnails)
	{
		if($path == ABSOLUTE_PATH . "/images/thumbnails")
		{
			return;
		}

		$filesystem = array();
		$files 		= scandir($path, SCANDIR_SORT_ASCENDING);
		$filetype 	= $this->input->get_post("type");

		// Look for directories first.
		foreach($files as $file)
		{
			set_time_limit(30);

			if($file == "." || $file == "..")
			{
				continue;
			}
			else if(is_dir($path . "/" . $file))
			{
				$x = new StdClass();
				$x->path = str_replace(ABSOLUTE_PATH . "/", "", $path . "/" . $file);
				$x->type = "directory";
				$x->files = $this->files($path . "/" . $file, false);
				$filesystem[] = $x;
			}
		}

		// Look for files second.
		foreach($files as $file)
		{
			set_time_limit(30);

			if($file == "." || $file == ".." || $file == ".htaccess")
			{
				continue;
			}
			else if(is_file($path . "/" . $file))
			{
				if(preg_match('/@2x\.(jpe?g|png|gif|webp)/i', $file))
				{
					// High DPI copies will automatically be handled silently.
					continue;
				}
				else if(!preg_match('/\.(jpe?g|png|gif|webp|ico)/i', $file) && $filetype == "images")
				{
					// If we're only showing images and the type doesn't look to be an image, ignore it.
					continue;
				}
				else if(preg_match('/index\.html/', $file))
				{
					// Skip index.html because it is never relevant.
					continue;
				}
				else if(preg_match('/\.htaccess/', $file))
				{
					// Skip .htaccess because it is never relevant.
					continue;
				}
				else if(preg_match('/^(ui-bg_|ui-icons_)/', $file))
				{
					// Skip jQuery UI image files.
					continue;
				}
				else if(preg_match('/\.(jpe?g|png|gif|webp).json$/i', $file))
				{
					// Skip metadata files.
					continue;
				}

				$size 		= false;
				$highdpi 	= false;

				$x = new StdClass();
				$x->path = str_replace(ABSOLUTE_PATH, "", $path . "/" . $file) . "?t=" . time();
				$x->type = "file";
				$x->info = "";

				if($generate_thumbnails && preg_match('/\.(jpe?g|png|gif|webp)/i', $file))
				{
					$size 			= getimagesize($path . "/" . $file);
					$highdpi 		= file_exists($path . "/" . preg_replace('/\.(jpe?g|png|gif|webp)$/i', '@2x.$1', $file));
					$x->thumbnail 	= Mainframe::thumbnail($path . "/" . $file, 200, 150, false, Mainframe::site()->webp ? IMAGETYPE_WEBP : null) . "?t=" . time();
				}
				else if(preg_match('/\.ico/i', $file))
				{
					require_once(APPLICATION_PATH . "/third_party/class.ico.php");

					$size 			= getimagesize($path . "/" . $file);
					$x->thumbnail 	= $x->path . "?t=" . time();
					$ico = new Ico($path . "/" . $file);

					foreach($ico->formats as $f)
					{
						$x->info .= ($x->info ? ", " : "") . $f["Width"] . 'x' . $f["Height"];
					}

					// Prevent code below from adding size a second time.
					$size = false;
				}

				if($size !== false)
				{
					$x->info = $size[0] . 'x' . $size[1];
				}

				if($highdpi)
				{
					$x->info .= ($x->info ? " / " : "") . '<span class="nowrap">High DPI</span>';
				}

				if(($mod = filemtime($path . "/" . $file)) !== false)
				{
					$x->info .= ($x->info ? "<br>" : "") . '<span class="nowrap">' . date("M j, Y", $mod) . '</span>';
				}

				if(($bytes = filesize($path . "/" . $file)) !== false)
				{
					$x->info .= ($x->info ? " / " : "") . '<span class="nowrap">' . bytes_format($bytes) . '</span>';
				}

				$filesystem[] = $x;
			}
		}

		return $filesystem;
	}

	private function documents($path)
	{
		$this->load->model("document");
		$documents = $this->document->loadByDirectory(Mainframe::active_site_id(), $path, $this->current_user);

		$filesystem = array();
		$files 		= scandir($path, SCANDIR_SORT_ASCENDING);
		$filetype 	= $this->input->get_post("type");

		// Look for directories first.
		foreach($files as $file)
		{
			set_time_limit(30);

			if($file == "." || $file == "..")
			{
				continue;
			}
			else if(is_dir($path . "/" . $file))
			{
				$x = new StdClass();
				$x->path = str_replace(ABSOLUTE_PATH . "/", "", $path . "/" . $file);
				$x->type = "directory";
				$x->files = $this->documents($path . "/" . $file);
				$filesystem[] = $x;
			}
		}

		// Look for files second.
		foreach($documents as $doc)
		{
			set_time_limit(30);

			$x 				= new StdClass();
			$x->path 		= str_replace(ABSOLUTE_PATH, "", $path . "/" . $doc->filename);
			$x->document_id = $doc->document_id;
			$x->url  		= $doc->url;
			$x->title 		= $doc->title;
			$x->category_id = $doc->category_id;
			$x->acl 		= $this->acl->loadByDocumentID($doc->document_id);
			$x->published 	= $doc->published;
			$x->type 		= "document";
			$x->info 		= "";
			// $x->info 		.= ($x->info ? " / " : "") . '<span class="nowrap"><em class="fas fa-fw" aria-hidden="true"></em> ';
			// $trimit 		= false;

			// foreach($x->acl as $acl)
			// {
			// 	if($acl->read)
			// 	{
			// 		$x->info .= $acl->role . ", ";
			// 		$trimit = true;
			// 	}
			// }

			// if($trimit)
			// {
			// 	$x->info = substr($x->info, 0, -2);
			// }

			// $x->info .= '</span>';

			if(($mod = filemtime($path . "/" . $doc->filename)) !== false)
			{
				$x->info .= ($x->info ? " / " : "") . '<span class="nowrap">' . date("l, F jS, Y \a\\t g:ia", $mod) . '</span>';
			}

			if(($bytes = filesize($path . "/" . $doc->filename)) !== false)
			{
				$x->info .= ($x->info ? " / " : "") . '<span class="nowrap">' . bytes_format($bytes) . '</span>';
			}

			$filesystem[] 	= $x;
		}

		return $filesystem;
	}

	public function new_directory()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		$cwd 		= $this->input->post("cwd");
		$filename 	= str_replace(array("'", '"'), "", $this->input->post("filename"));

		if(mkdir(ABSOLUTE_PATH . "/" . $cwd . "/" . $filename))
		{
			http_response_code(200);
		}
		else
		{
			http_response_code(409);
			die("You don't have filesystem permission to create a directory here.");
		}
	}

	private function strip_time($path)
	{
		if(strrpos($path, "?") === false)
		{
			return $path;
		}

		return substr($path, 0, strrpos($path, "?"));
	}

	public function directorySettings()
	{
		if(!$this->current_user->has(ACTION_MEDIA))
		{
			return $this->denied($this->data);
		}

		$done 		= false;
		$path 		= $this->input->post("path");
		$settings 	= new StdClass();
		$settings->force_crop 	= false;
		$settings->high_dpi 	= true;

		if($path == "images")
		{
			// $settings->note = "This directory contains all the images available to use on your website. Some subdirectories will have specific size requirements.";
			$done = true;
		}
		else if($path == "documents")
		{
			// $settings->note = "This directory contains all of your <a href='/admin/documents'>documents</a>.";
			$done = true;
		}
		else if($path == "videos")
		{
			$settings->note = "This directory contains videos used by \"video\" modules. For optimal viewing, videos should be in MP4 and WebM formats and have the same filename. ie: myvideo.mp4 and myvideo.webm";
			$done = true;
		}

		// Check for "gallery" modules
		if(!$done)
		{
			$gpath = $path;

			while(true)
			{
				$query = $this->db->query("SELECT mi.tag
				                          FROM module_instances AS mi
				                          INNER JOIN modules AS m USING(module_id)
				                          INNER JOIN module_settings AS ms USING(module_instance_id)
				                          WHERE m.file_name='gallery'
				                          AND ms.`key`='directory' AND ms.`value`=?
				                          ORDER BY mi.published DESC LIMIT 1",
				                          array($gpath));	// If there are more than 1, take one that's published.

				if($query->num_rows())
				{
					$row = $query->row();
					$settings->note = "This directory is used by the \"" . $row->tag . "\" module. Images should be large, perhaps 1280x1024. You will be able to crop your images during the <a href='#' onclick='nvfinder_upload_dialog(); return false;'>upload process</a>.";
					$settings->crop_width 	= 1280;
					$settings->crop_height 	= 1024;
					$settings->high_dpi 	= false;
					$done = true;
					break;
				}

				// Gallery modules can include subdirectories,
				// so lets check if the parent is a gallery path.
				if(strpos($gpath, "/") === false)
				{
					break;
				}
				else
				{
					$gpath = substr($gpath, 0, strrpos($gpath, "/"));
				}
			}
		}

		// Check for "gallery" content types.
		if(!$done)
		{
			$gpath = $path;

			while(true)
			{
				$query = $this->db->query("SELECT p.*
				                          FROM pages AS p
				                          INNER JOIN content_types AS t USING(content_type_id)
				                          INNER JOIN content_values AS v USING(page_id)
				                          WHERE t.filename='gallery'
				                          AND v.`key`='directory' AND v.`value`=?
				                          LIMIT 1",
				                          array($gpath));	// If there are more than 1, take one that's published.

				if($query->num_rows())
				{
					$row = $query->row();
					$settings->note = "This directory is used by a gallery page. Images should be large, perhaps 1280x1024. You will be able to crop your images during the <a href='#' onclick='nvfinder_upload_dialog(); return false;'>upload process</a>.";
					$settings->crop_width 	= 1280;
					$settings->crop_height 	= 1024;
					$settings->high_dpi 	= false;
					$done = true;
					break;
				}

				// Gallery modules can include subdirectories,
				// so lets check if the parent is a gallery path.
				if(strpos($gpath, "/") === false)
				{
					break;
				}
				else
				{
					$gpath = substr($gpath, 0, strrpos($gpath, "/"));
				}
			}
		}

		// Check for "galleria" modules
		if(!$done)
		{
			$query = $this->db->query("SELECT mi.module_instance_id, mi.tag
			                          FROM module_instances AS mi
			                          INNER JOIN modules AS m USING(module_id)
			                          INNER JOIN module_settings AS ms USING(module_instance_id)
			                          WHERE m.file_name='galleria'
			                          AND ms.`key`='directory' AND ms.`value`=?
			                          ORDER BY mi.published DESC LIMIT 1",
			                          array($path));	// If there are more than 1, take one that's published.

			if($query->num_rows())
			{
				$row = $query->row();
				$settings->note = "This directory is used by the \"" . $row->tag . "\" module. Images should be large, perhaps 1280x1024. You will be able to crop your images during the <a href='#' onclick='nvfinder_upload_dialog(); return false;'>upload process</a>.";
				$settings->crop_width 	= 1280;
				$settings->crop_height 	= 1024;
				$settings->high_dpi 	= false;
				$done = true;
			}
		}

		// Check for "slideshow_bxslider" modules
		if(!$done)
		{
			$query = $this->db->query("SELECT mi.module_instance_id, mi.tag
			                          FROM module_instances AS mi
			                          INNER JOIN modules AS m USING(module_id)
			                          INNER JOIN module_settings AS ms USING(module_instance_id)
			                          WHERE m.file_name='slideshow_bxslider'
			                          AND ms.`key`='directory' AND ms.`value`=?
			                          ORDER BY mi.published DESC LIMIT 1",
			                          array($path));	// If there are more than 1, take one that's published.

			if($query->num_rows())
			{
				$row = $query->row();
				$settings->note = "This directory is used by the \"" . $row->tag . "\" module. Images should be large, perhaps 1920x500. You will be able to crop your images during the <a href='#' onclick='nvfinder_upload_dialog(); return false;'>upload process</a>.";
				$settings->crop_width 	= 1920;
				$settings->crop_height 	= 500;
				$settings->high_dpi 	= false;
				$done = true;
			}
		}

		// Check for "slideshow_nivoslider" modules
		if(!$done)
		{
			$query = $this->db->query("SELECT mi.module_instance_id, mi.tag
			                          FROM module_instances AS mi
			                          INNER JOIN modules AS m USING(module_id)
			                          INNER JOIN module_settings AS ms USING(module_instance_id)
			                          WHERE m.file_name='slideshow_nivoslider'
			                          AND ms.`key`='directory' AND ms.`value`=?
			                          ORDER BY mi.published DESC LIMIT 1",
			                          array($path));	// If there are more than 1, take one that's published.

			if($query->num_rows())
			{
				$row = $query->row();
				$module_instance_id = $row->module_instance_id;

				$query = $this->db->query("SELECT ms.`key`, ms.`value`
			                          FROM module_instances AS mi
			                          INNER JOIN module_settings AS ms USING(module_instance_id)
			                          WHERE mi.module_instance_id=?
			                          AND (ms.`key`='max_width' OR ms.`key`='max_height')",
			                          array($module_instance_id));

				foreach($query->result() as $row2)
				{
					if($row2->key == "max_width")
					{
						$settings->crop_width = $row2->value;
					}
					else if($row2->key == "max_height")
					{
						$settings->crop_height = $row2->value;
					}
				}

				$settings->note = "This directory is used by the \"" . $row->tag . "\" module. Images must be " . $settings->crop_width . "x" . $settings->crop_height . ". You will be able to crop your images during the <a href='#' onclick='nvfinder_upload_dialog(); return false;'>upload process</a>.";
				$settings->force_crop 	= true;
				$settings->high_dpi 	= false;
				$done = true;
			}
		}

		// Check for "slideshow_slippry" modules
		if(!$done)
		{
			$query = $this->db->query("SELECT mi.module_instance_id, mi.tag
			                          FROM module_instances AS mi
			                          INNER JOIN modules AS m USING(module_id)
			                          INNER JOIN module_settings AS ms USING(module_instance_id)
			                          WHERE m.file_name='slideshow_slippry'
			                          AND ms.`key`='directory' AND ms.`value`=?
			                          ORDER BY mi.published DESC LIMIT 1",
			                          array($path));	// If there are more than 1, take one that's published.

			if($query->num_rows())
			{
				$row = $query->row();
				$settings->note = "This directory is used by the \"" . $row->tag . "\" module. Images should be large, perhaps 1920x500. You will be able to crop your images during the <a href='#' onclick='nvfinder_upload_dialog(); return false;'>upload process</a>.";
				$settings->crop_width 	= 1920;
				$settings->crop_height 	= 500;
				$settings->high_dpi 	= false;
				$done = true;
			}
		}

		// Check for "random_image" modules
		if(!$done)
		{
			$query = $this->db->query("SELECT mi.module_instance_id, mi.tag
			                          FROM module_instances AS mi
			                          INNER JOIN modules AS m USING(module_id)
			                          INNER JOIN module_settings AS ms USING(module_instance_id)
			                          WHERE m.file_name='random_image'
			                          AND ms.`key`='directory' AND ms.`value`=?
			                          ORDER BY mi.published DESC LIMIT 1",
			                          array($path));	// If there are more than 1, take one that's published.

			if($query->num_rows())
			{
				$row = $query->row();
				$module_instance_id = $row->module_instance_id;

				$query = $this->db->query("SELECT ms.`key`, ms.`value`
			                          FROM module_instances AS mi
			                          INNER JOIN module_settings AS ms USING(module_instance_id)
			                          WHERE mi.module_instance_id=?
			                          AND (ms.`key`='max_width' OR ms.`key`='max_height')",
			                          array($module_instance_id));

				foreach($query->result() as $row2)
				{
					if($row2->key == "max_width")
					{
						$settings->crop_width = $row2->value;
					}
					else if($row2->key == "max_height")
					{
						$settings->crop_height = $row2->value;
					}
				}

				$settings->note = "This directory is used by the \"" . $row->tag . "\" module. Images must be " . $settings->crop_width . "x" . $settings->crop_height . ". You will be able to crop your images during the <a href='#' onclick='nvfinder_upload_dialog(); return false;'>upload process</a>.";
				$settings->force_crop 	= true;
				$settings->high_dpi 	= false;
				$done = true;
			}
		}

		$this->spit_json($settings);
	}

	public function metadata()
	{
		$file = substr($this->input->post("filename"), 0, strpos($this->input->post("filename"), "?"));

		if(file_exists(ABSOLUTE_PATH . $file . ".json"))
		{
			$data              = json_decode(file_get_contents(ABSOLUTE_PATH . $file . ".json"));
			$data->alt         = $data->alt;
			$data->title       = $data->title;
			$data->description = $data->description;
		}
		else
		{
			$data              = new StdClass();
			$data->alt         = "";
			$data->title       = "";
			$data->description = "";
		}

		$this->spit_json($data);
	}

	public function metadata_save()
	{
		$file              = substr($this->input->post("filename"), 0, strpos($this->input->post("filename"), "?"));
		$data              = new StdClass();
		$data->alt         = $this->input->post("alt");
		$data->title       = $this->input->post("title");
		$data->description = $this->input->post("description");

		// If all values are blank, remove the file.
		if($data->alt === "" && $data->title === "" && $data->description === "")
		{
			if(file_exists(ABSOLUTE_PATH . $file . ".json"))
			{
				unlink(ABSOLUTE_PATH . $file . ".json");
				$this->cache->clean();
			}
			http_response_code(200);
		}
		else
		{
			if(file_put_contents(ABSOLUTE_PATH . $file . ".json", json_encode($data)))
			{
				$this->cache->clean();
				http_response_code(200);
			}
			else
			{
				http_response_code(409);
			}
		}
	}
}
