Subversion Repositories oidplus

Compare Revisions

Regard whitespace Rev 634 → Rev 635

/trunk/plugins/viathinksoft/publicPages/095_attachments/OIDplusPagePublicAttachments.class.php
0,0 → 1,447
<?php
 
/*
* OIDplus 2.0
* Copyright 2019 - 2021 Daniel Marschall, ViaThinkSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
 
if (!defined('INSIDE_OIDPLUS')) die();
 
class OIDplusPagePublicAttachments extends OIDplusPagePluginPublic {
 
const DIR_UNLOCK_FILE = 'oidplus_upload.dir';
 
private static function checkUploadDir($dir) {
if (!is_dir($dir)) {
throw new OIDplusException(_L('The attachment directory "%1" is not existing.', $dir));
}
 
$realdir = realpath($dir);
if ($realdir === false) {
throw new OIDplusException(_L('The attachment directory "%1" cannot be resolved (realpath).', $dir));
}
 
$unlock_file = $realdir . DIRECTORY_SEPARATOR . self::DIR_UNLOCK_FILE;
if (!file_exists($unlock_file)) {
throw new OIDplusException(_L('Unlock file "%1" is not existing in attachment directory "%2".', self::DIR_UNLOCK_FILE, $dir));
}
 
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
// Linux check 1: Check for critical directories
if (self::isCriticalLinuxDirectory($realdir)) {
throw new OIDplusException(_L('The attachment directory must not be inside a critical system directory!'));
}
 
// Linux check 2: Check file owner
$file_owner_a = fileowner(OIDplus::localpath().'index.php');
if ($file_owner_a === false) {
$file_owner_a = -1;
$file_owner_a_name = '???';
} else {
$tmp = posix_getpwuid($file_owner_a);
$file_owner_a_name = $tmp !== false ? $tmp['name'] : 'UID '.$file_owner_a;
}
 
$file_owner_b = fileowner($unlock_file);
if ($file_owner_b === false) {
$file_owner_b = -1;
$file_owner_b_name = '???';
} else {
$tmp = posix_getpwuid($file_owner_b);
$file_owner_b_name = $tmp !== false ? $tmp['name'] : 'UID '.$file_owner_b;
}
 
if ($file_owner_a != $file_owner_b) {
throw new OIDplusException(_L('Owner of unlock file "%1" is wrong. It is "%2", but it should be "%3".', $unlock_file, $file_owner_b_name, $file_owner_a_name));
}
} else {
// Windows check 1: Check for critical directories
if (self::isCriticalWindowsDirectory($realdir)) {
throw new OIDplusException(_L('The attachment directory must not be inside a critical system directory!'));
}
 
// Note: We will not query the file owner in Windows systems.
// It would be possible, however, on Windows systems, the file
// ownership is rather hidden to the user and the user needs
// to go into several menus and windows in order to see/change
// the owner. We don't want to over-complicate it to the Windows admin.
}
}
 
private static function isCriticalWindowsDirectory($dir) {
$dir .= '\\';
$windir = isset($_SERVER['SystemRoot']) ? $_SERVER['SystemRoot'].'\\' : 'C:\\Windows\\';
if (stripos($dir,$windir) === 0) return true;
return false;
}
 
private static function isCriticalLinuxDirectory($dir) {
if ($dir == '/') return true;
$dir .= '/';
if (strpos($dir,'/bin/') === 0) return true;
if (strpos($dir,'/boot/') === 0) return true;
if (strpos($dir,'/dev/') === 0) return true;
if (strpos($dir,'/etc/') === 0) return true;
if (strpos($dir,'/lib') === 0) return true;
if (strpos($dir,'/opt/') === 0) return true;
if (strpos($dir,'/proc/') === 0) return true;
if (strpos($dir,'/root/') === 0) return true;
if (strpos($dir,'/run/') === 0) return true;
if (strpos($dir,'/sbin/') === 0) return true;
if (strpos($dir,'/sys/') === 0) return true;
if (strpos($dir,'/tmp/') === 0) return true;
if (strpos($dir,'/usr/bin/') === 0) return true;
if (strpos($dir,'/usr/include/') === 0) return true;
if (strpos($dir,'/usr/lib') === 0) return true;
if (strpos($dir,'/usr/sbin/') === 0) return true;
if (strpos($dir,'/usr/src/') === 0) return true;
if (strpos($dir,'/var/cache/') === 0) return true;
if (strpos($dir,'/var/lib') === 0) return true;
if (strpos($dir,'/var/lock/') === 0) return true;
if (strpos($dir,'/var/log/') === 0) return true;
if (strpos($dir,'/var/mail/') === 0) return true;
if (strpos($dir,'/var/opt/') === 0) return true;
if (strpos($dir,'/var/run/') === 0) return true;
if (strpos($dir,'/var/spool/') === 0) return true;
if (strpos($dir,'/var/tmp/') === 0) return true;
return false;
}
 
public static function getUploadDir($id=null) {
// Get base path
$cfg = OIDplus::config()->getValue('attachment_upload_dir', '');
$cfg = trim($cfg);
if ($cfg === '') {
$basepath = OIDplus::localpath() . 'userdata' . DIRECTORY_SEPARATOR . 'attachments';
} else {
$basepath = $cfg;
}
 
try {
self::checkUploadDir($basepath);
} catch (Exception $e) {
$error = _L('This functionality is not available due to a misconfiguration');
if (OIDplus::authUtils()->isAdminLoggedIn()) {
$error .= ': '.$e->getMessage();
} else {
$error .= '. '._L('Please notify the system administrator. After they log-in, they can see the reason at this place.');
}
throw new OIDplusException($error);
}
 
// Get object-specific path
if (!is_null($id)) {
$obj = OIDplusObject::parse($id);
if ($obj === null) throw new OIDplusException(_L('Invalid object "%1"',$id));
 
$path_v1 = $basepath . DIRECTORY_SEPARATOR . $obj->getLegacyDirectoryName();
$path_v1_bug = $basepath . $obj->getLegacyDirectoryName();
$path_v2 = $basepath . DIRECTORY_SEPARATOR . $obj->getDirectoryName();
 
if (is_dir($path_v1)) return $path_v1; // backwards compatibility
if (is_dir($path_v1_bug)) return $path_v1_bug; // backwards compatibility
return $path_v2;
} else {
return $basepath;
}
}
 
private function raMayDelete() {
return OIDplus::config()->getValue('attachments_allow_ra_delete', 0);
}
 
private function raMayUpload() {
return OIDplus::config()->getValue('attachments_allow_ra_upload', 0);
}
 
public function action($actionID, $params) {
 
if ($actionID == 'deleteAttachment') {
_CheckParamExists($params, 'id');
$id = $params['id'];
$obj = OIDplusObject::parse($id);
if ($obj === null) throw new OIDplusException(_L('Invalid object "%1"',$id));
if (!$obj->userHasWriteRights()) throw new OIDplusException(_L('Authentication error. Please log in as admin, or as the RA of "%1" to upload an attachment.',$id));
 
if (!OIDplus::authUtils()->isAdminLoggedIn() && !$this->raMayDelete()) {
throw new OIDplusException(_L('The administrator has disabled deleting attachments by RAs.'));
}
 
_CheckParamExists($params, 'filename');
$req_filename = $params['filename'];
if (strpos($req_filename, '/') !== false) throw new OIDplusException(_L('Illegal file name'));
if (strpos($req_filename, '\\') !== false) throw new OIDplusException(_L('Illegal file name'));
if (strpos($req_filename, '..') !== false) throw new OIDplusException(_L('Illegal file name'));
if (strpos($req_filename, chr(0)) !== false) throw new OIDplusException(_L('Illegal file name'));
 
$uploaddir = self::getUploadDir($id);
$uploadfile = $uploaddir . DIRECTORY_SEPARATOR . basename($req_filename);
 
if (!file_exists($uploadfile)) throw new OIDplusException(_L('File does not exist'));
@unlink($uploadfile);
if (file_exists($uploadfile)) {
OIDplus::logger()->log("[ERR]OID($id)+[ERR]A!", "Attachment file '".basename($uploadfile)."' could not be deleted from object '$id' (problem with permissions?)");
$msg = _L('Attachment file "%1" could not be deleted from object "%2" (problem with permissions?)',basename($uploadfile),$id);
if (OIDplus::authUtils()->isAdminLoggedIn()) {
throw new OIDplusException($msg);
} else {
throw new OIDplusException($msg.'. '._L('Please contact the system administrator.'));
}
} else {
// If it was the last file, delete the empty directory
$ary = glob($uploaddir . DIRECTORY_SEPARATOR . '*');
if (count($ary) == 0) @rmdir($uploaddir);
}
 
OIDplus::logger()->log("[OK]OID($id)+[?INFO/!OK]OIDRA($id)?/[?INFO/!OK]A?", "Deleted attachment '".basename($uploadfile)."' from object '$id'");
 
return array("status" => 0);
 
} else if ($actionID == 'uploadAttachment') {
_CheckParamExists($params, 'id');
$id = $params['id'];
$obj = OIDplusObject::parse($id);
if ($obj === null) throw new OIDplusException(_L('Invalid object "%1"',$id));
if (!$obj->userHasWriteRights()) throw new OIDplusException(_L('Authentication error. Please log in as admin, or as the RA of "%1" to upload an attachment.',$id));
 
if (!OIDplus::authUtils()->isAdminLoggedIn() && !$this->raMayUpload()) {
throw new OIDplusException(_L('The administrator has disabled uploading attachments by RAs.'));
}
 
if (!isset($_FILES['userfile'])) {
throw new OIDplusException(_L('Please choose a file.'));
}
 
if (!OIDplus::authUtils()->isAdminLoggedIn()) {
$banned = explode(',', OIDplus::config()->getValue('attachments_block_extensions', ''));
foreach ($banned as $ext) {
$ext = trim($ext);
if ($ext == '') continue;
if (strtolower(substr(basename($_FILES['userfile']['name']), -strlen($ext)-1)) == strtolower('.'.$ext)) {
throw new OIDplusException(_L('The file extension "%1" is banned by the administrator (it can be uploaded by the administrator though)',$ext));
}
}
}
 
$req_filename = $_FILES['userfile']['name'];
if (strpos($req_filename, '/') !== false) throw new OIDplusException(_L('Illegal file name'));
if (strpos($req_filename, '\\') !== false) throw new OIDplusException(_L('Illegal file name'));
if (strpos($req_filename, '..') !== false) throw new OIDplusException(_L('Illegal file name'));
if (strpos($req_filename, chr(0)) !== false) throw new OIDplusException(_L('Illegal file name'));
 
$uploaddir = self::getUploadDir($id);
$uploadfile = $uploaddir . DIRECTORY_SEPARATOR . basename($req_filename);
 
if (!is_dir($uploaddir)) {
@mkdir($uploaddir, 0777, true);
if (!is_dir($uploaddir)) {
OIDplus::logger()->log("[ERR]OID($id)+[ERR]A!", "Upload attachment '".basename($uploadfile)."' to object '$id' failed: Cannot create directory '".basename($uploaddir)."' (problem with permissions?)");
$msg = _L('Upload attachment "%1" to object "%2" failed',basename($uploadfile),$id).': '._L('Cannot create directory "%1" (problem with permissions?)',basename($uploaddir));
if (OIDplus::authUtils()->isAdminLoggedIn()) {
throw new OIDplusException($msg);
} else {
throw new OIDplusException($msg.'. '._L('Please contact the system administrator.'));
}
}
}
 
if (!@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
OIDplus::logger()->log("[ERR]OID($id)+[ERR]A!", "Upload attachment '".basename($uploadfile)."' to object '$id' failed: Cannot move uploaded file into directory (problem with permissions?)");
$msg = _L('Upload attachment "%1" to object "%2" failed',basename($uploadfile),$id).': '._L('Cannot move uploaded file into directory (problem with permissions?)');
if (OIDplus::authUtils()->isAdminLoggedIn()) {
throw new OIDplusException($msg);
} else {
throw new OIDplusException($msg.'. '._L('Please contact the system administrator.'));
}
}
 
OIDplus::logger()->log("[OK]OID($id)+[?INFO/!OK]OIDRA($id)?/[?INFO/!OK]A?", "Uploaded attachment '".basename($uploadfile)."' to object '$id'");
 
return array("status" => 0);
} else {
throw new OIDplusException(_L('Unknown action ID'));
}
}
 
public function init($html=true) {
OIDplus::config()->prepareConfigKey('attachments_block_extensions', 'Block file name extensions being used in file attachments (comma separated)', 'exe,scr,pif,bat,com,vbs,cmd', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
});
OIDplus::config()->prepareConfigKey('attachments_allow_ra_delete', 'Allow that RAs delete file attachments? (0=no, 1=yes)', '0', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
if (!is_numeric($value) || ($value < 0) || ($value > 1)) {
throw new OIDplusException(_L('Please enter a valid value (0=no, 1=yes).'));
}
});
OIDplus::config()->prepareConfigKey('attachments_allow_ra_upload', 'Allow that RAs upload file attachments? (0=no, 1=yes)', '0', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
if (!is_numeric($value) || ($value < 0) || ($value > 1)) {
throw new OIDplusException(_L('Please enter a valid value (0=no, 1=yes).'));
}
});
 
$info_txt = 'Alternative directory for attachments. It must contain a file named "';
$info_txt .= self::DIR_UNLOCK_FILE;
$info_txt .= '"';
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
$info_txt .= ' with the same owner as index.php';
}
$info_txt .= '. If this setting is empty, then the userdata directory is used.';
OIDplus::config()->prepareConfigKey('attachment_upload_dir', $info_txt, '', OIDplusConfig::PROTECTION_EDITABLE, function($value) {
if (trim($value) !== '') {
self::checkUploadDir($value);
}
});
}
 
public function gui($id, &$out, &$handled) {
// Nothing
}
 
public function publicSitemap(&$out) {
// Nothing
}
 
public function tree(&$json, $ra_email=null, $nonjs=false, $req_goto='') {
return false;
}
 
private static function convert_filesize($bytes, $decimals = 2){
$size = array(_L('Bytes'),_L('KiB'),_L('MiB'),_L('GiB'),_L('TiB'),_L('PiB'),_L('EiB'),_L('ZiB'),_L('YiB'));
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . @$size[$factor];
}
 
public function implementsFeature($id) {
if (strtolower($id) == '1.3.6.1.4.1.37476.2.5.2.3.2') return true; // modifyContent
if (strtolower($id) == '1.3.6.1.4.1.37476.2.5.2.3.3') return true; // beforeObject*, afterObject*
if (strtolower($id) == '1.3.6.1.4.1.37476.2.5.2.3.4') return true; // whois*Attributes
return false;
}
 
public function modifyContent($id, &$title, &$icon, &$text) {
// Interface 1.3.6.1.4.1.37476.2.5.2.3.2
 
$output = '';
$doshow = false;
 
try {
$upload_dir = self::getUploadDir($id);
$files = glob($upload_dir . DIRECTORY_SEPARATOR . '*');
$found_files = false;
 
$obj = OIDplusObject::parse($id);
if ($obj === null) throw new OIDplusException(_L('Invalid object "%1"',$id));
$can_upload = OIDplus::authUtils()->isAdminLoggedIn() || ($this->raMayUpload() && $obj->userHasWriteRights());
$can_delete = OIDplus::authUtils()->isAdminLoggedIn() || ($this->raMayDelete() && $obj->userHasWriteRights());
 
if (OIDplus::authUtils()->isAdminLoggedIn()) {
$output .= '<p>'._L('Admin info: The directory is %1','<b>'.htmlentities($upload_dir).'</b>').'</p>';
$doshow = true;
}
 
$output .= '<div id="fileattachments_table" class="table-responsive">';
$output .= '<table class="table table-bordered table-striped">';
$output .= '<tr>';
$output .= '<th>'._L('Filename').'</th>';
$output .= '<th>'._L('Size').'</th>';
$output .= '<th>'._L('File type').'</th>';
$output .= '<th>'._L('Download').'</th>';
if ($can_delete) $output .= '<th>'._L('Delete').'</th>';
$output .= '</tr>';
foreach ($files as $file) {
if (is_dir($file)) continue;
 
$output .= '<tr>';
$output .= '<td>'.htmlentities(basename($file)).'</td>';
$output .= '<td>'.htmlentities(self::convert_filesize(filesize($file), 0)).'</td>';
$lookup_files = array(
OIDplus::localpath().'userdata/attachments/filetypes$'.OIDplus::getCurrentLang().'.conf',
OIDplus::localpath().'userdata/attachments/filetypes.conf',
OIDplus::localpath().'vendor/danielmarschall/fileformats/filetypes$'.OIDplus::getCurrentLang().'.local', // not recommended
OIDplus::localpath().'vendor/danielmarschall/fileformats/filetypes.local', // not recommended
OIDplus::localpath().'vendor/danielmarschall/fileformats/filetypes$'.OIDplus::getCurrentLang().'.conf',
OIDplus::localpath().'vendor/danielmarschall/fileformats/filetypes.conf'
);
$output .= '<td>'.htmlentities(VtsFileTypeDetect::getDescription($file, $lookup_files)).'</td>';
 
$output .= ' <td><button type="button" name="download_'.md5($file).'" id="download_'.md5($file).'" class="btn btn-success btn-xs download" onclick="OIDplusPagePublicAttachments.downloadAttachment('.js_escape(OIDplus::webpath(__DIR__)).', current_node,'.js_escape(basename($file)).')">'._L('Download').'</button></td>';
if ($can_delete) {
$output .= ' <td><button type="button" name="delete_'.md5($file).'" id="delete_'.md5($file).'" class="btn btn-danger btn-xs delete" onclick="OIDplusPagePublicAttachments.deleteAttachment(current_node,'.js_escape(basename($file)).')">'._L('Delete').'</button></td>';
}
 
$output .= '</tr>';
$doshow = true;
$found_files = true;
}
 
if (!$found_files) $output .= '<tr><td colspan="'.($can_delete ? 5 : 4).'"><i>'._L('No attachments').'</i></td></tr>';
 
$output .= '</table></div>';
 
if ($can_upload) {
$output .= '<form action="javascript:void(0);" onsubmit="return OIDplusPagePublicAttachments.uploadAttachmentOnSubmit(this);" enctype="multipart/form-data" id="uploadAttachmentForm">';
$output .= '<input type="hidden" name="id" value="'.htmlentities($id).'">';
$output .= '<div>'._L('Add a file attachment').':<input type="file" name="userfile" value="" id="fileAttachment">';
$output .= '<br><input type="submit" value="'._L('Upload').'"></div>';
$output .= '</form>';
$doshow = true;
}
} catch (Exception $e) {
$doshow = true;
$output = '<p>'.$e->getMessage().'</p>';
}
 
$output = '<h2>'._L('File attachments').'</h2>' .
'<div class="container box">' .
$output .
'</div>';
if ($doshow) $text .= $output;
}
 
public function beforeObjectDelete($id) {} // Interface 1.3.6.1.4.1.37476.2.5.2.3.3
public function afterObjectDelete($id) {
// Interface 1.3.6.1.4.1.37476.2.5.2.3.3
// Delete the attachment folder including all files in it (note: Subfolders are not possible)
$uploaddir = self::getUploadDir($id);
if ($uploaddir != '') {
$ary = glob($uploaddir . DIRECTORY_SEPARATOR . '*');
foreach ($ary as $a) @unlink($a);
@rmdir($uploaddir);
if (is_dir($uploaddir)) {
OIDplus::logger()->log("[WARN]OID($id)+[WARN]A!", "Attachment directory '$uploaddir' could not be deleted during the deletion of the OID");
}
}
}
public function beforeObjectUpdateSuperior($id, &$params) {} // Interface 1.3.6.1.4.1.37476.2.5.2.3.3
public function afterObjectUpdateSuperior($id, &$params) {} // Interface 1.3.6.1.4.1.37476.2.5.2.3.3
public function beforeObjectUpdateSelf($id, &$params) {} // Interface 1.3.6.1.4.1.37476.2.5.2.3.3
public function afterObjectUpdateSelf($id, &$params) {} // Interface 1.3.6.1.4.1.37476.2.5.2.3.3
public function beforeObjectInsert($id, &$params) {} // Interface 1.3.6.1.4.1.37476.2.5.2.3.3
public function afterObjectInsert($id, &$params) {} // Interface 1.3.6.1.4.1.37476.2.5.2.3.3
 
public function tree_search($request) {
return false;
}
 
public function whoisObjectAttributes($id, &$out) {
// Interface 1.3.6.1.4.1.37476.2.5.2.3.4
 
$files = glob(self::getUploadDir($id) . DIRECTORY_SEPARATOR . '*');
foreach ($files as $file) {
$out[] = 'attachment-name: '.basename($file);
$out[] = 'attachment-url: '.OIDplus::webpath(__DIR__).'download.php?id='.urlencode($id).'&filename='.urlencode(basename($file));
}
 
}
public function whoisRaAttributes($email, &$out) {} // Interface 1.3.6.1.4.1.37476.2.5.2.3.4
}