<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package clearFusionCMS
 * @copyright Copyright (c) 2012 - 2016 clearFusionCMS. All rights reserved.
 * @link http://clearfusioncms.com
 */

/**
 * cfBackendModulesAction Class
 *
 * Action class to list installed and available modules as well as allow the install/uninstall of the module.
 *
 * @package clearFusionCMS
 */
class cfBackendModulesAction extends cfBackendControllerAction {

	/**
	 * Delete a folder and all child folders and files.
	 *
	 * @param string $folder The folder to delete
	 */
	protected function delFolder($folder) {
		$files = glob($folder . '/*');

		foreach($files as $f) {
			if(is_dir($f)) {
				$this->delFolder($f);
				rmdir($f);
			}
			else
				unlink($f);
		}
	}

	/**
	 * Build a list of the files and folders in a tree.
	 *
	 * @param array $list The list of files and folders.
	 * @param string $folder The folder to index.
	 */
	protected function buildFileList(& $list, $folder) {
		$l = strlen(FLMODPATH);
		$files = glob($folder . '/*');
		foreach($files as $f) {
			if(is_dir($f)) {
				$list[] = substr($f, $l) . '/';
				$this->buildFileList($list, $f);
			}
			else
				$list[] = substr($f, $l);
		}
	}

	/**
	 * Unpack a module zip file.
	 *
	 * @param string $fileName The name of the file holding the module to unpack.
	 * @return string The result string to show to the user.
	 */
	protected function unpackModule($fileName) {
		$zip = new ZipArchive();
		if($zip->open($fileName) === true && $zip->numFiles > 1) {

			$isCore = $isModule = false;

			// Test if core archive
			for($i = 0; $i < $zip->numFiles; $i++) {
				if(preg_match('#system/modules/content/contentInfo\.ini$#', $zip->getNameIndex($i))) {
					$isCore = true;
					break;
				}
			}

			// If core then extract the archive
			if($isCore) {
				cfModuleAudit::uploadedCore();
				if($zip->extractTo(FLROOTPATH))
					$msg = $this->alertMessage('Success', __('The application archive has been extracted.', 'cfBackend'));
				else
					$msg = $this->alertMessage('Error', __('Unable to extract the application archive.', 'cfBackend'));
			}
			else {
				// Get the module name
				$moduleName = rtrim($zip->getNameIndex(0), '/');
				cfModuleAudit::uploadedModule($moduleName);

				// Calc path and name of module info
				$moduleInfo = "$moduleName/{$moduleName}Info.ini";

				// Test if the module info is available in the archive
				$isModule = false;
				for($i = 0; $i < $zip->numFiles; $i++) {
					if($moduleInfo == $zip->getNameIndex($i)) {
						$isModule = true;
						break;
					}
				}

				// If module in archive then unpack it
				if($isModule) {
					// Get the list of existing files on disk
					$existing = array($moduleName . '/');
					$this->buildFileList($existing, FLMODPATH . $moduleName);
					$existing = array_flip($existing);

					// Find the files on disk that are not needed
					for($i = 0; $i < $zip->numFiles; $i++)
						unset($existing[$zip->getNameIndex($i)]);

					// Remove files that are not in archive
					foreach($existing as $f => $i) {
						if(!preg_match('/\.[mp]o$/', $f))
							@unlink(FLMODPATH . $f);
					}

					// Extract the archive
					if($zip->extractTo(FLMODPATH))
						$msg = $this->alertMessage('Success', __('The module archive has been extracted.', 'cfBackend'));
					else
						$msg = $this->alertMessage('Error', __('Unable to extract the module archive.', 'cfBackend'));
				}
				else
					$msg = $this->alertMessage('Error', __('The file does not appear to be a valid module archive.', 'cfBackend'));
			}

			$zip->close();
		}
		else
			$msg = $this->alertMessage('Error', __('The file does not appear to be a valid container for a module.', 'cfBackend'));

		return $msg;
	}

	/**
	 * Process the action.
	 */
	function process() {
		$this->testPermission('core.modules');
		$this->breadcrumbTrail->addCrumb(__('Modules', 'cfBackend'), $this->assemble('', 'modules'));

		$mm = new flModuleMgr($this->db);

		// Build form for uploading modules
		$uploadMsg = $dloadMsg = '';
		$formUpld = new flForm($this->request, 'uploadmoduleform', $this->assemble('', 'modules'), true);
		if(is_writable(FLMODPATH) && is_writable(sys_get_temp_dir())) {
			$file = new flFormElementFile($formUpld, 'file');
			$file->setLabel(__('Module ZIP File', 'cfBackend'))->setCSS('primary')
				->setDescription(__('Choose the module ZIP file to upload.', 'cfBackend'));

			// Calc max upload size
			$maxUpload = (int)ini_get('upload_max_filesize');
			$maxPost = (int)ini_get('post_max_size');
			$memoryLimit = (int)ini_get('memory_limit');
			$uploadSize = min($maxUpload, $maxPost, $memoryLimit);

			$uri = new flFormElementUri($formUpld, 'uri');
			$uri->setLabel(__('Download Module From', 'cfBackend'))
				->setDescription(__('The web address to download the module ZIP file from.', 'cfBackend'));

			$submit = new flFormElementSubmit($formUpld, 'Upload');
			$submit->setCaption(__('Upload Module', 'cfBackend'))->setCSS('primary')
				->setDescription(sprintf(__('Maximum file upload size is %dMBytes.', 'cfBackend'), $uploadSize));
		}
		else
			new flFormElementPane($formUpld, 'info', $this->alertMessage('Info', __('You do not have permission to write to the modules folder via PHP therefore the install feature has been disabled.', 'cfBackend')));

		if($formUpld->isSubmitted() && $formUpld->isValid()) {
			if($file->fileUploaded())
				$uploadMsg = $this->unpackModule($file->getTmpFileName());
			else if($uri->getValue() != '') {
				$client = new flHttpClient($uri->getValue());
				if($client->sendRequest() && $client->getResponseCode() == 200) {
					$fileName = tempnam(sys_get_temp_dir(), 'cf_');

					// Save the file
					if(file_put_contents($fileName, $client->getBody())) {
						$uploadMsg = $this->unpackModule($fileName);
						unlink($fileName);
					}
					else
						$uploadMsg = $this->alertMessage('Error', __('Failed to write module archive to disk.', 'cfBackend'));
				}
				else
					$uploadMsg = $this->alertMessage('Error', __('Module archive failed to download.', 'cfBackend'));
			}
			else
				$uploadMsg = $this->alertMessage('Error', __('Please select a module archive file to upload or enter the address to download the module from and try again.', 'cfBackend'));
		}

		$content = '';
		$deleting = false;
		$msg = '';
		$data = $this->request->getParam('*');
		if(count($data) == 2) {

			if($data[0] == 'install' && preg_match('/^[a-z0-9_.]+$/', $data[1])) {
				$info = $mm->getInfo($data[1]);
				if($mm->install($data[1])) {
					cfModuleAudit::installed($info->name, $info->version);
					$this->cache->flush();
					$msg = $this->alertMessage('Success', sprintf(__('Installed the module %s.', 'cfBackend'), $info === false ? $data[1] : $info->name));
				}
				else {
					$msg = $this->alertMessage('Error', sprintf(__('Failed to install module %s.', 'cfBackend'), $info === false ? $data[1] : $info->name) . '<br /> ' . $mm->getLastError());
				}
			}
			else if($data[0] == 'uninstall' && preg_match('/^[a-z0-9_.]+$/', $data[1])) {
				$this->breadcrumbTrail->addCrumb(__('Uninstall', 'cfBackend'), $this->assemble('', 'modules', array('uninstall', $data[1])));

				$info = $mm->getInfo($data[1]);

				$form = new flForm($this->request, 'removemodule', $this->assemble('', 'modules', array('uninstall', $data[1])), true);

				$modname = new flFormElementText($form, 'modname', $info->name);
				$modname->setLabel(__('Module Name', 'cfBackend'))
					->setReadonly()
					->setDisabled();

				if(is_writable(FLMODPATH)) {
					$delFiles = new flFormElementCheckbox($form, 'delfiles');
					$delFiles->setLabel(__('Delete Module Files', 'cfBackend'))
						->setDescription('If ticked then the module files will be deleted once the module has been uninstalled.', 'cfBackend');
				}

				$delSubmit = new flFormElementSubmit($form, 'deletemod');
				$delSubmit->setCaption(__('Uninstall Module &amp; Permanently Delete All Data', 'cfBackend'))
					->setCSS('danger')
					->setDescription($this->alertMessage('Warning', __('This action cannot be undone and will delete all data associated with the module!', 'cfBackend')));

				if($form->isSubmitted() && $form->isValid()) {
					if($mm->uninstall($data[1])) {
						clearFusionCMS::getEventManager()->notify(new flEvent('system.moduleRemoved', array('module' => $data[1])));

						cfModuleAudit::removed($info->name, $info->version);

						// If deleting module files
						if(isset($delFiles) && $delFiles->getValue()) {
							$this->delFolder(FLMODPATH . $data[1]);
							rmdir(FLMODPATH . $data[1]);
						}

						$this->response->redirectTo($this->assemble('', 'modules', array(), true));
					}
					else
						$msg = $this->alertMessage('Error', sprintf(__('Failed to uninstall module %s.', 'cfBackend'), $info === false ? $data[1] : $info->name));
					$this->cache->flush();
				}

				$content = $this->renderBlock(__('Uninstall Module', 'cfBackend'), $form->render($this->tpl->getCodeBuffer()), '', $msg);

				$deleting = true;
			}
		}

		if(!$deleting) {
			$manifest = array(); // Version manifest cache

			// Build the list of installed modules
			$installed = new flWidgetTable();
			$installed->setClass('tablewidget bordered striped rowhover responsive')
				->setHeadings(__('Name', 'cfBackend'), __('Version', 'cfBackend'), __('Latest Version', 'cfBackend'), __('Eligible Version', 'cfBackend'), __('Description', 'cfBackend'), __('License', 'cfBackend'), __('Operation', 'cfBackend'))
				->setColData('name', 'version', 'latestver', 'eligibleVer', 'description', 'licence', 'ops')
				->skipEncode('name', 'roles', 'ops', 'description', 'licence');

			$sysLicence = new flConfigDb('cfSystemLicence', $this->db);

			$inst = $mm->getInstalled();
			foreach($inst as $name => $ver) {
				if($info = $mm->getInfo($name)) {
					$row = array();

					// Read the manifest
					$row['latestver'] = '';
					if(isset($info->versionManifest)) {
						// If not already read manifest
						if(!array_key_exists($info->versionManifest, $manifest)) {
							$client = new flHttpClient($info->versionManifest);
							$client->setPostData(array(
								'key' => $sysLicence->code
							));
							$client->setTimeout(5);
							if($client->sendRequest() && $client->getResponseCode() == 200) {
								$manifest[$info->versionManifest] = json_decode($client->getBody());
							}
							else
								$manifest[$info->versionManifest] = null;
						}

						// If have manifest info for module
						$checkName = $name != 'content' ? $name : (function_exists('ioncube_file_is_encoded') && ioncube_file_is_encoded() ? 'clearFusionCMSFree' : 'clearFusionCMSPro');
						if($manifest[$info->versionManifest] !== null && isset($manifest[$info->versionManifest]->$checkName->version)) {
							$row['latestver'] = $manifest[$info->versionManifest]->$checkName->version;
							$row['eligibleVer'] = $manifest[$info->versionManifest]->$checkName->eligibleVersion;
						}
					}

					// If newer version on disk then upgrade
					if($ver != $info->version)
						$mm->upgrade($name);

					$row['version'] = $info->version;
//					$row['icon'] = '<img src="system/modules/' . $name . '/' . $name . 'Icon.png" width="64" height="64" />';
					$row['name'] = '<span style="white-space: nowrap;">' . $info->name . '</span>';
					if($info->get('canUninstall', true))
						$row['ops'] = '<a href="' . $this->assemble('', 'modules', array('uninstall', $name)) . '" class="modulelistuninstall danger">' . __('Uninstall', 'cfBackend') . '</a>';
					if(isset($info->licence))
						$row['licence'] = "<a href=\"$info->licence\">" . __('View License', 'cfBackend') . "</a>";

					$row['description'] = $info->description . "<br /><span class=\"modulelistauth\">$info->author | <a href=\"http://$info->site\">$info->site</a></span>";
					$installed->addRow($row);
				}
			}

			$avail = $mm->getAvailable();
			foreach($avail as $name) {
				if($info = $mm->getInfo($name)) {
					$row = array();

					// Read the manifest
					$row['latestver'] = '';
					if(isset($info->versionManifest)) {
						// If not already read manifest
						if(!isset($manifest[$info->versionManifest])) {
							$client = new flHttpClient($info->versionManifest);
							if($client->sendRequest() && $client->getResponseCode() == 200) {
								$manifest[$info->versionManifest] = json_decode($client->getBody());
							}
							else
								$manifest[$info->versionManifest] = null;
						}

						// If have manifest info for module
						$checkName = $name != 'content' ? $name : (function_exists('ioncube_file_is_encoded') && ioncube_file_is_encoded() ? 'clearFusionCMSFree' : 'clearFusionCMSPro');
						if($manifest[$info->versionManifest] !== null && isset($manifest[$info->versionManifest]->$checkName->version))
							$row['latestver'] = $manifest[$info->versionManifest]->$checkName->version;
					}

					$row['version'] = $info->version;
					$row['ops'] = '<a href="' . $this->assemble('', 'modules', array('install', $name)) . '" class="modulelistinstall">' . __('Install', 'cfBackend') . '</a>';
					$row['icon'] = '<img src="system/modules/' . $name . '/' . $name . 'Icon.png" width="64" height="64" />';
					$row['name'] = '<span style="white-space: nowrap;">' . $info->name . '</span>';
					if(isset($info->licence))
						$row['licence'] = "<a href=\"$info->licence\">" . __('View License', 'cfBackend') . "</a>";

					$row['description'] = $info->description . "<br /><span class=\"modulelistauth\">$info->author | <a href=\"http://$info->site\">$info->site</a></span>";
					$installed->addRow($row);
				}
			}

			$content .= $this->renderBlock(__('Modules', 'cfBackend'), $installed->render(), '', $msg);
		}

		$this->renderPage(__('Modules', 'cfBackend'), $content, $this->renderBlock(__('Install Module', 'cfBackend'), $formUpld->render($this->tpl->getCodeBuffer()), '', $uploadMsg));
	}
}
