DaPortal
<?php //$Id$
							//Copyright (c) 2012-2016 Pierre Pronchery <khorben@defora.org>
							//This file is part of DeforaOS Web DaPortal
							//
							//This program is free software: you can redistribute it and/or modify
							//it under the terms of the GNU General Public License as published by
							//the Free Software Foundation, version 3 of the License.
							//
							//This program 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 General Public License for more details.
							//
							//You should have received a copy of the GNU General Public License
							//along with this program.  If not, see <http://www.gnu.org/licenses/>.
							//BrowserModule
							class BrowserModule extends Module
							{
								//public
								//methods
								//essential
								//BrowserModule::call
								public function call(Engine $engine, Request $request, $internal = 0)
								{
									if(($action = $request->getAction()) === FALSE)
										$action = 'default';
									if($internal)
										switch($action)
										{
											case 'actions':
												return $this->$action($engine,
														$request);
											default:
												return FALSE;
										}
									switch($action)
									{
										case 'default':
										case 'download':
										case 'upload':
											$action = 'call'.$action;
											return $this->$action($engine, $request);
										default:
											return new ErrorResponse(_('Invalid action'),
												Response::$CODE_ENOENT);
									}
								}
								//protected
								//methods
								//accessors
								//BrowserModule::canUpload
								protected function canUpload(Engine $engine, Request $request = NULL,
										$content = FALSE, &$error = FALSE)
								{
									$credentials = $engine->getCredentials();
									$error = _('Permission denied');
									$root = $this->getRoot($engine);
									//check for the global setting
									if(!$this->configGet('upload'))
										return FALSE;
									//check for anonymous submissions
									$error = _('Anonymous submissions are not allowed');
									if($credentials->getUserID() == 0)
										if(!$this->configGet('anonymous'))
											return FALSE;
									if($content === FALSE)
										return TRUE;
									//check for idempotence
									$error = _('The request expired or is invalid');
									if($request === NULL || $request->isIdempotent())
										return FALSE;
									//check for write permissions
									$error = _('Could not lookup the path');
									if(($path = $this->getPath($engine, $request)) === FALSE)
										return FALSE;
									$error = _('Permission denied');
									return posix_access($root.'/'.$path, POSIX_W_OK) ? TRUE : FALSE;
								}
								//BrowserModule::getDate
								protected function getDate($time)
								{
									$format = _('%d/%m/%Y %H:%M:%S');
									return Date::formatTimestamp($time, $format);
								}
								//BrowserModule::getGroup
								protected function getGroup($gid)
								{
									static $cache = array();
									if(isset($cache[$gid]))
										return $cache[$gid];
									$cache[$gid] = (function_exists('posix_getgrgid')
										&& ($gr = posix_getgrgid($gid)) !== FALSE)
										? $gr['name'] : $gid;
									return $cache[$gid];
								}
								//BrowserModule::getPath
								protected function getPath(Engine $engine, Request $request)
								{
									$root = $this->getRoot($engine);
									$from = array('-');
									$to = array('?');
									if(($path = $request->getTitle()) === FALSE)
										return '/';
									$p = str_replace($from, $to, $path);
									if(($res = glob($root.'/'.$p, GLOB_NOESCAPE)) !== FALSE
											&& count($res) == 1)
										$path = substr($res[0], strlen($root));
									return $this->helperSanitizePath($path);
								}
								//BrowserModule::getPermissions
								protected function getPermissions($mode)
								{
									return Common::getPermissions($mode);
								}
								//BrowserModule::getRoot
								protected function getRoot(Engine $engine)
								{
									if(($root = $this->configGet('root')) === FALSE)
									{
										$message = 'The browser repository is not configured';
										$engine->log(LOG_WARNING, $message);
										$root = '/tmp';
									}
									return $root;
								}
								//BrowserModule::getToolbar
								protected function getToolbar(Engine $engine, $path, $directory = FALSE)
								{
									$toolbar = new PageElement('toolbar');
									if(($parent = dirname($path)) != $path)
									{
										$r = new Request($this->name, FALSE, FALSE,
												ltrim($parent, '/'));
										//XXX change the label to "Browse" for files
										$toolbar->append('button', array('request' => $r,
												'stock' => 'updir',
												'text' => _('Parent directory')));
									}
									$r = new Request($this->name, FALSE, FALSE, ltrim($path, '/'));
									$toolbar->append('button', array('request' => $r,
											'stock' => 'refresh', 'text' => _('Refresh')));
									if($directory === FALSE)
									{
										$r = new Request($this->name, 'download', FALSE,
												ltrim($path, '/'));
										$toolbar->append('button', array('request' => $r,
												'stock' => 'download',
												'text' => _('Download')));
									}
									else if($this->canUpload($engine))
									{
										$r = new Request($this->name, 'upload', FALSE,
												ltrim($path, '/'));
										$toolbar->append('button', array('request' => $r,
												'stock' => 'upload',
												'text' => _('Upload')));
									}
									return $toolbar;
								}
								//BrowserModule::getUser
								protected function getUser($uid)
								{
									static $cache = array();
									if(isset($cache[$uid]))
										return $cache[$uid];
									$cache[$uid] = (function_exists('posix_getpwuid')
										&& ($pw = posix_getpwuid($uid)) !== FALSE)
										? $pw['name'] : $uid;
									return $cache[$uid];
								}
								//useful
								//actions
								//BrowserModule::actions
								protected function actions(Engine $engine, Request $request)
								{
									$ret = array();
									if($request->get('user') !== FALSE
											|| $request->get('group') !== FALSE)
										return $ret;
									if($request->get('admin') != 0)
										return $ret;
									return $ret;
								}
								//calls
								//BrowserModule::callDefault
								protected function callDefault(Engine $engine, Request $request)
								{
									//obtain the path requested
									$path = $this->getPath($engine, $request);
									$title = _('Browser: ').$path;
									$page = new Page(array('title' => $title));
									//title
									$page->append('title', array('stock' => $this->name,
											'text' => $title));
									//view
									$this->helperDisplay($engine, $page, $path);
									return new PageResponse($page);
								}
								//BrowserModule::callDownload
								protected function callDownload(Engine $engine, Request $request)
								{
									$root = $this->getRoot($engine);
									$error = _('Could not download the file requested');
									//obtain the path requested
									$path = $this->getPath($engine, $request);
									if(($fp = fopen($root.'/'.$path, 'rb')) !== FALSE)
									{
										$ret = new StreamResponse($fp);
										if(($type = Mime::getType($engine, $path)) !== FALSE)
											$ret->setType($type);
										$ret->setFilename(basename($path));
										return $ret;
									}
									$title = _('Browser: ').$path;
									$page = new Page(array('title' => $title));
									$page->append('title', array('stock' => $this->name,
											'text' => $title));
									$page->append('dialog', array('type' => 'error',
											'text' => $error));
									return new PageResponse($page);
								}
								//BrowserModule::callUpload
								protected function callUpload(Engine $engine, Request $request)
								{
									$root = $this->getRoot($engine);
									//check permissions
									$error = _('Unknown error');
									if($this->canUpload($engine, $request, FALSE, $error) === FALSE)
										return new ErrorResponse($error, Response::$CODE_EPERM);
									//obtain the path requested
									$path = $this->getPath($engine, $request);
									//create the page
									$title = _('Browser: ')._('Upload to ').$path;
									$page = new Page(array('title' => $title));
									//title
									$page->append('title', array('stock' => $this->name,
											'text' => $title));
									//toolbar
									//FIXME let stat() vs lstat() be configurable
									$error = _('Could not open the file or directory requested');
									if(($st = @stat($root.'/'.$path)) === FALSE)
										return new ErrorResponse($error);
									if(($st['mode'] & Common::$S_IFDIR) == Common::$S_IFDIR)
										$toolbar = $this->getToolbar($engine, $path, TRUE);
									else
										$toolbar = $this->getToolbar($engine, $path, FALSE);
									$page->append($toolbar);
									//process the request
									if(($error = $this->_uploadProcess($engine, $request, $path))
											=== FALSE)
										return $this->_uploadSuccess($engine, $request, $path,
												$page);
									else if(is_string($error))
										$page->append('dialog', array('type' => 'error',
												'text' => $error));
									//form
									$r = new Request($this->name, 'upload', FALSE, ltrim($path,
											'/'));
									$form = $page->append('form', array('request' => $r));
									$form->append('filechooser', array('name' => 'files[]'));
									$r = new Request($this->name, FALSE, FALSE, ltrim($path, '/'));
									$form->append('button', array('request' => $r,
											'stock' => 'cancel',
											'target' => '_cancel', 'text' => _('Cancel')));
									$form->append('button', array('type' => 'submit',
											'name' => 'action', 'value' => '_upload',
											'text' => _('Upload')));
									return new PageResponse($page);
								}
								protected function _uploadProcess(Engine $engine, Request $request,
										$path)
								{
									$ret = TRUE;
									$forbidden = array('.', '..');
									//XXX UNIX supports backward slashes in filenames
									$delimiters = array('/', '\\');
									//verify the request
									if($request->isIdempotent())
										return TRUE;
									//upload the file(s)
									if(!isset($_FILES['files'])
											|| count($_FILES['files']['error']) == 0)
										return TRUE;
									//check known errors
									$count = 0;
									foreach($_FILES['files']['error'] as $k => $v)
									{
										if($v == UPLOAD_ERR_NO_FILE)
											continue;
										else if($v != UPLOAD_ERR_OK)
											return _('An error occurred');
										foreach($forbidden as $f)
											if($_FILES['files']['name'][$k] == $f)
												return _('Forbidden filename');
										foreach($delimiters as $d)
											if(strpos($_FILES['files']['name'][$k], $d)
													!== FALSE)
												return _('An error occurred');
										$count++;
									}
									if($count == 0)
										return _('No file uploaded');
									if(is_file($path) && $count != 1)
										//overwrite files one file at a time
										return _('Destination is not a directory');
									//store each file uploaded
									foreach($_FILES['files']['error'] as $k => $v)
									{
										if($_FILES['files']['error'] == UPLOAD_ERR_NO_FILE)
											continue;
										$res = $this->_uploadProcessFile($engine, $request,
												$path, $_FILES['files']['tmp_name'][$k],
												$_FILES['files']['name'][$k], $content);
										if($res === FALSE)
											return _('Internal server error');
									}
									return FALSE;
								}
								protected function _uploadProcessFile(Engine $engine, Request $request,
										$parent, $pathname, $filename, &$content)
								{
									$root = $this->getRoot($engine);
									$dst = $root.'/'.$parent.'/'.$filename;
									return move_uploaded_file($pathname, $dst);
								}
								protected function _uploadSuccess(Engine $engine, Request $request,
										$path, $page)
								{
									$r = new Request($this->name, FALSE, FALSE, ltrim($path, '/'));
									return $this->helperRedirect($engine, $r, $page,
											$this->text_upload_progress);
								}
								//helpers
								//BrowserModule::helperDisplay
								protected function helperDisplay(Engine $engine, PageElement $page,
										$path)
								{
									$root = $this->getRoot($engine);
									$error = _('Could not open the file or directory requested');
									//FIXME let stat() vs lstat() be configurable
									if(($st = @stat($root.'/'.$path)) === FALSE)
										return $page->append('dialog', array('type' => 'error',
												'text' => $error));
									if(($st['mode'] & Common::$S_IFDIR) == Common::$S_IFDIR)
									{
										$toolbar = $this->getToolbar($engine, $path, TRUE);
										$page->append($toolbar);
										$this->helperDisplayDirectory($engine, $page, $root,
												$path);
									}
									else
									{
										$toolbar = $this->getToolbar($engine, $path, FALSE);
										$page->append($toolbar);
										$this->helperDisplayFile($engine, $page, $root, $path,
												$st);
									}
								}
								//BrowserModule::helperDisplayDirectory
								protected function helperDisplayDirectory(Engine $engine,
										PageElement $page, $root, $path)
								{
									$error = _('Could not open the directory requested');
									if(($dir = @opendir($root.'/'.$path)) === FALSE)
										return $page->append('dialog', array('type' => 'error',
											'text' => $error));
									$path = rtrim($path, '/');
									$columns = array('icon' => '', 'title' => _('Title'),
											'user' => _('User'), 'group' => _('Group'),
											'size' => _('Size'), 'date' => _('Date'),
											'mode' => _('Permissions'));
									$view = $page->append('treeview', array('columns' => $columns));
									//obtain (and sort) all entries
									$entries = array();
									while(($de = readdir($dir)) !== FALSE)
										$entries[] = $de;
									closedir($dir);
									asort($entries);
									foreach($entries as $de)
									{
										//skip "." and ".."
										if($de == '.' || $de == '..')
											continue;
										$fullpath = $root.'/'.$path.'/'.$de;
										$st = lstat($fullpath);
										$row = $view->append('row');
										if($st['mode'] & Common::$S_IFDIR)
											$icon = Mime::getIconByType($engine,
												'inode/directory', 16);
										else
											$icon = Mime::getIcon($engine, $de, 16);
										$icon = new PageElement('image', array(
												'source' => $icon));
										$row->setProperty('icon', $icon);
										$r = new Request($this->name, FALSE, FALSE,
												ltrim($path.'/'.$de, '/'));
										$link = new PageElement('link', array(
												'request' => $r, 'text' => $de));
										$row->setProperty('title', $link);
										$row->setProperty('user', $this->getUser($st['uid']));
										$row->setProperty('group', $this->getGroup($st['gid']));
										$row->setProperty('size', Common::getSize($st['size']));
										$row->setProperty('date', $this->getDate($st['mtime']));
										//permissions
										$permissions = $this->getPermissions($st['mode']);
										$permissions = new PageElement('label', array(
												'class' => 'preformatted',
												'text' => $permissions));
										$row->setProperty('mode', $permissions);
									}
								}
								//BrowserModule::helperDisplayFile
								protected function helperDisplayFile(Engine $engine, PageElement $page,
										$root, $path, $st)
								{
									$hbox = $page->append('hbox');
									$col1 = $hbox->append('vbox');
									$col2 = $hbox->append('vbox');
									//filename
									$col1->append('label', array('class' => 'bold',
											'text' => _('Filename:')));
									$r = new Request($this->name, 'download', FALSE,
											ltrim($path, '/'));
									$link = new PageElement('link', array('request' => $r,
											'text' => basename($path)));
									$col2->append($link);
									//type
									$this->_displayFileField($col1, $col2, _('Type:'),
											Mime::getType($engine, basename($path)));
									//user
									$this->_displayFileField($col1, $col2, _('User:'),
											$this->getUser($st['uid']));
									//group
									$this->_displayFileField($col1, $col2, _('Group:'),
											$this->getGroup($st['gid']));
									//permissions
									$this->_displayFileField($col1, $col2, _('Permissions:'),
											$this->getPermissions($st['mode']),
											'preformatted');
									//size
									$this->_displayFileField($col1, $col2, _('Size:'),
											Common::getSize($st['size']));
									//creation time
									$this->_displayFileField($col1, $col2, _('Created on:'),
											$this->getDate($st['ctime']));
									//modification time
									$this->_displayFileField($col1, $col2, _('Last modified:'),
											$this->getDate($st['mtime']));
									//access time
									$this->_displayFileField($col1, $col2, _('Last access:'),
											$this->getDate($st['atime']));
								}
								private function _displayFileField($col1, $col2, $field, $value,
										$class = FALSE)
								{
									$col1->append('label', array('class' => 'bold',
											'text' => $field.' '));
									$col2->append('label', array('class' => $class,
											'text' => $value));
								}
								//DownloadModule::helperRedirect
								protected function helperRedirect(Engine $engine, Request $request,
										PageElement $page, $text = FALSE)
								{
									//XXX duplicated from ContentModule
									if($text === FALSE)
										$text = $this->text_redirect_progress;
									$page->set('location', $engine->getURL($request));
									$page->set('refresh', 30);
									$box = $page->append('vbox');
									$box->append('label', array('text' => $text));
									$box = $box->append('hbox');
									$text = _('If you are not redirected within 30 seconds, please ');
									$box->append('label', array('text' => $text));
									$box->append('link', array('text' => _('click here'),
											'request' => $request));
									$box->append('label', array('text' => '.'));
									return new PageResponse($page);
								}
								//BrowserModule::helperSanitizePath
								protected function helperSanitizePath($path)
								{
									$path = '/'.ltrim($path, '/');
									$path = str_replace('/./', '/', $path);
									//FIXME really implement '..'
									if(strcmp($path, '/..') == 0 || strpos($path, '/../') !== FALSE)
										return '/';
									return $path;
								}
								//properties
								protected $text_redirect_progress = 'Redirection in progress, please wait...';
								protected $text_upload_progress = 'Upload in progress, please wait...';
							}
							?>
							