v1.2.0 - Merge branch 'dev'
This commit is contained in:
		
						commit
						e3b075c4be
					
				| @ -6,6 +6,10 @@ DirectorySlash Off | ||||
| RewriteEngine On | ||||
| RewriteRule ^api(/.*)?$ /index.php?api=$1 [L,QSA] | ||||
| 
 | ||||
| RewriteEngine On | ||||
| RewriteOptions AllowNoSlash | ||||
| RewriteRule ^files$ /files/ [L,QSA] | ||||
| 
 | ||||
| RewriteEngine On | ||||
| RewriteOptions AllowNoSlash | ||||
| RewriteRule ^((\.idea|\.git|src|test|core)(/.*)?)$ /index.php?site=$1 [L,QSA] | ||||
|  | ||||
							
								
								
									
										0
									
								
								src/.gitignore → adminPanel/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										0
									
								
								src/.gitignore → adminPanel/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
								
								
									
										5
									
								
								adminPanel/.idea/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										5
									
								
								adminPanel/.idea/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| # Default ignored files | ||||
| /shelf/ | ||||
| /workspace.xml | ||||
| # Editor-based HTTP Client requests | ||||
| /httpRequests/ | ||||
							
								
								
									
										12
									
								
								adminPanel/.idea/adminPanel.iml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										12
									
								
								adminPanel/.idea/adminPanel.iml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <module type="WEB_MODULE" version="4"> | ||||
|   <component name="NewModuleRootManager"> | ||||
|     <content url="file://$MODULE_DIR$"> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/temp" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/.tmp" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/tmp" /> | ||||
|     </content> | ||||
|     <orderEntry type="inheritedJdk" /> | ||||
|     <orderEntry type="sourceFolder" forTests="false" /> | ||||
|   </component> | ||||
| </module> | ||||
							
								
								
									
										6
									
								
								adminPanel/.idea/inspectionProfiles/Project_Default.xml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										6
									
								
								adminPanel/.idea/inspectionProfiles/Project_Default.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| <component name="InspectionProjectProfileManager"> | ||||
|   <profile version="1.0"> | ||||
|     <option name="myName" value="Project Default" /> | ||||
|     <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||
|   </profile> | ||||
| </component> | ||||
							
								
								
									
										8
									
								
								adminPanel/.idea/modules.xml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										8
									
								
								adminPanel/.idea/modules.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="ProjectModuleManager"> | ||||
|     <modules> | ||||
|       <module fileurl="file://$PROJECT_DIR$/.idea/adminPanel.iml" filepath="$PROJECT_DIR$/.idea/adminPanel.iml" /> | ||||
|     </modules> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										6
									
								
								adminPanel/.idea/vcs.xml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										6
									
								
								adminPanel/.idea/vcs.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="VcsDirectoryMappings"> | ||||
|     <mapping directory="$PROJECT_DIR$/.." vcs="Git" /> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										40134
									
								
								adminPanel/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										40134
									
								
								adminPanel/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -16,13 +16,14 @@ | ||||
|     "react-dom": "^16.13.1", | ||||
|     "react-draft-wysiwyg": "^1.14.5", | ||||
|     "react-router-dom": "^5.2.0", | ||||
|     "react-scripts": "^3.4.1", | ||||
|     "react-scripts": "^4.0.3", | ||||
|     "react-select": "^3.1.0", | ||||
|     "react-tooltip": "^4.2.7", | ||||
|     "sanitize-html": "^1.27.0" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "build": "webpack --mode production && mv dist/main.js ../js/admin.min.js" | ||||
|     "build": "webpack --mode production && mv dist/main.js ../js/admin.min.js", | ||||
|     "debug": "react-scripts start" | ||||
|   }, | ||||
|   "eslintConfig": { | ||||
|     "extends": "react-app" | ||||
							
								
								
									
										9
									
								
								adminPanel/public/index.html
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										9
									
								
								adminPanel/public/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| <html> | ||||
| <head> | ||||
|   <link rel="stylesheet" href="/css/bootstrap.min.css"> | ||||
|   <link rel="stylesheet" href="/css/fontawesome.min.css"> | ||||
| </head> | ||||
| <body> | ||||
| <div id="root"></div> | ||||
| </body> | ||||
| </html> | ||||
| @ -5,7 +5,7 @@ export default function Footer() { | ||||
|     return ( | ||||
|         <footer className={"main-footer"}> | ||||
|             Theme: <strong>Copyright © 2014-2019 <a href={"http://adminlte.io"}>AdminLTE.io</a>. <b>Version</b> 3.0.3</strong>  | ||||
|             CMS: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> 1.0.3 | ||||
|             CMS: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> 1.2.0 | ||||
|         </footer> | ||||
|     ) | ||||
| } | ||||
| @ -36,7 +36,6 @@ export default function Header(props) { | ||||
|     } | ||||
| 
 | ||||
|     function onToggleSidebar() { | ||||
|         console.log(document.body.classList); | ||||
|         let classes = document.body.classList; | ||||
|         if (classes.contains("sidebar-collapse")) { | ||||
|             classes.remove("sidebar-collapse"); | ||||
| @ -30,7 +30,8 @@ class AdminDashboard extends React.Component { | ||||
|     this.state = { | ||||
|       loaded: false, | ||||
|       dialog: { onClose: () => this.hideDialog() }, | ||||
|       notifications: [ ] | ||||
|       notifications: [ ], | ||||
|       filesPath: null | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
| @ -57,12 +58,35 @@ class AdminDashboard extends React.Component { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   fetchFilesPath() { | ||||
|     this.api.getRoutes().then((res) => { | ||||
|       if (!res.success) { | ||||
|         this.showDialog("Error fetching routes: " + res.msg, "Error fetching routes"); | ||||
|       } else { | ||||
|         for (const route of res.routes) { | ||||
|           if (route.target === "\\Documents\\Files") { | ||||
|             // prepare the path patterns, e.g. '/files(/.*)?' => '/files'
 | ||||
|             let path = route.request; | ||||
|             path = path.replace(/\(.*\)([?*])/g, ''); // remove optional and 0-n groups
 | ||||
|             path = path.replace(/.\*/g, ''); // remove .*
 | ||||
|             path = path.replace(/\[.*]\*/g, ''); // remove []*
 | ||||
|             path = path.replace(/(.*)\+/g, "$1"); // replace 1-n groups with one match
 | ||||
|             // todo: add some more rules, but we should have most of the cases now
 | ||||
|             this.setState({...this.state, filesPath: path }); | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount() { | ||||
|     this.api.fetchUser().then(Success => { | ||||
|       if (!Success) { | ||||
|         document.location = "/admin"; | ||||
|       } else { | ||||
|         this.fetchNotifications(); | ||||
|         this.fetchFilesPath(); | ||||
|         setInterval(this.onUpdate.bind(this), 60*1000); | ||||
|         this.setState({...this.state, loaded: true}); | ||||
|       } | ||||
| @ -83,7 +107,7 @@ class AdminDashboard extends React.Component { | ||||
| 
 | ||||
|     return <Router> | ||||
|         <Header {...this.controlObj} notifications={this.state.notifications} /> | ||||
|         <Sidebar {...this.controlObj} notifications={this.state.notifications} /> | ||||
|         <Sidebar {...this.controlObj} notifications={this.state.notifications} filesPath={this.state.filesPath} /> | ||||
|         <div className={"content-wrapper p-2"}> | ||||
|           <section className={"content"}> | ||||
|             <Switch> | ||||
| @ -7,7 +7,8 @@ export default function Sidebar(props) { | ||||
|     let parent = { | ||||
|         showDialog: props.showDialog || function() {}, | ||||
|         api: props.api, | ||||
|         notifications: props.notifications || [ ] | ||||
|         notifications: props.notifications || [ ], | ||||
|         filesPath: props.filesPath || null | ||||
|     }; | ||||
| 
 | ||||
|     function onLogout() { | ||||
| @ -71,6 +72,16 @@ export default function Sidebar(props) { | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     let filePath = parent.filesPath; | ||||
|     if (filePath) { | ||||
|         li.push(<li className={"nav-item"} key={"files"}> | ||||
|             <a href={filePath} className={"nav-link"} target={"_blank"} rel={"noopener noreferrer"}> | ||||
|                 <Icon icon={"folder"} className={"nav-icon"} /> | ||||
|                 <p>Files</p> | ||||
|             </a> | ||||
|         </li>); | ||||
|     } | ||||
| 
 | ||||
|     li.push(<li className={"nav-item"} key={"logout"}> | ||||
|         <a href={"#"} onClick={() => onLogout()} className={"nav-link"}> | ||||
|             <Icon icon={"arrow-left"} className={"nav-icon"} /> | ||||
| @ -162,8 +162,8 @@ namespace Api\ApiKey { | ||||
|       $this->loginRequired = true; | ||||
|     } | ||||
| 
 | ||||
|     public function execute($aValues = array()) { | ||||
|       if(!parent::execute($aValues)) { | ||||
|     public function execute($values = array()) { | ||||
|       if(!parent::execute($values)) { | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|  | ||||
| @ -53,6 +53,7 @@ namespace Api\Contact { | ||||
|       } | ||||
| 
 | ||||
|       $this->createNotification(); | ||||
|       $this->sendMail(); | ||||
| 
 | ||||
|       if (!$this->success) { | ||||
|         return $this->createError("The contact request was saved, but the server was unable to create a notification."); | ||||
| @ -110,6 +111,17 @@ namespace Api\Contact { | ||||
| 
 | ||||
|       return $this->success; | ||||
|     } | ||||
| 
 | ||||
|     private function sendMail() { | ||||
|       /*$email = $this->getParam("fromEmail"); | ||||
|       $settings = $this->user->getConfiguration()->getSettings(); | ||||
|       $request = new \Api\Mail\Send($this->user); | ||||
|       $this->success = $request->execute(array( | ||||
|         "to" => $settings->get, | ||||
|         "subject" => "[$siteName] Account Invitation", | ||||
|         "body" => $messageBody | ||||
|       ));*/ | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										1096
									
								
								core/Api/FileAPI.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										1096
									
								
								core/Api/FileAPI.class.php
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										56
									
								
								core/Api/Parameter/ArrayType.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										56
									
								
								core/Api/Parameter/ArrayType.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| <?php | ||||
| 
 | ||||
| namespace Api\Parameter; | ||||
| 
 | ||||
| class ArrayType extends Parameter { | ||||
| 
 | ||||
|   private Parameter $elementParameter; | ||||
|   public int $elementType; | ||||
|   public int $canBeOne; | ||||
| 
 | ||||
|   public function __construct($name, $elementType = Parameter::TYPE_MIXED, $canBeOne=false, $optional = FALSE, $defaultValue = NULL) { | ||||
|     $this->elementType = $elementType; | ||||
|     $this->elementParameter = new Parameter('', $elementType); | ||||
|     $this->canBeOne = $canBeOne; | ||||
|     parent::__construct($name, Parameter::TYPE_ARRAY, $optional, $defaultValue); | ||||
|   } | ||||
| 
 | ||||
|   public function parseParam($value) { | ||||
|     if(!is_array($value)) { | ||||
|       if (!$this->canBeOne) { | ||||
|         return false; | ||||
|       } else { | ||||
|         $value = array($value); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if ($this->elementType != Parameter::TYPE_MIXED) { | ||||
|       foreach ($value as &$element) { | ||||
|         if ($this->elementParameter->parseParam($element)) { | ||||
|           $element = $this->elementParameter->value; | ||||
|         } else { | ||||
|           return false; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     $this->value = $value; | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   public function getTypeName() { | ||||
|     $elementType = $this->elementParameter->getTypeName(); | ||||
|     return parent::getTypeName() . "($elementType)"; | ||||
|   } | ||||
| 
 | ||||
|   public function toString() { | ||||
|     $typeName = $this->getTypeName(); | ||||
|     $str = "$typeName $this->name"; | ||||
|     $defaultValue = (is_null($this->value) ? 'NULL' : (is_array($this->value) ? '[' . implode(",", $this->value) . ']' : $this->value)); | ||||
|     if($this->optional) { | ||||
|       $str = "[$str = $defaultValue]"; | ||||
|     } | ||||
| 
 | ||||
|     return $str; | ||||
|   } | ||||
| } | ||||
| @ -18,9 +18,11 @@ class Parameter { | ||||
|   const TYPE_RAW       = 8; | ||||
| 
 | ||||
|   // only json will work here i guess
 | ||||
|   // nope. also name[]=value
 | ||||
|   const TYPE_ARRAY     = 9; | ||||
|   const TYPE_MIXED     = 10; | ||||
| 
 | ||||
|   const names = array('Integer', 'Float', 'Boolean', 'String', 'Date', 'Time', 'DateTime', 'E-Mail', 'Raw', 'Array'); | ||||
|   const names = array('Integer', 'Float', 'Boolean', 'String', 'Date', 'Time', 'DateTime', 'E-Mail', 'Raw', 'Array', 'Mixed'); | ||||
| 
 | ||||
|   public string $name; | ||||
|   public $value; | ||||
|  | ||||
							
								
								
									
										65
									
								
								core/Api/PatchSQL.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										65
									
								
								core/Api/PatchSQL.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| <?php | ||||
| 
 | ||||
| namespace Api; | ||||
| 
 | ||||
| use Api\Parameter\StringType; | ||||
| use Configuration\DatabaseScript; | ||||
| use Objects\User; | ||||
| 
 | ||||
| class PatchSQL extends Request { | ||||
| 
 | ||||
|   public function __construct(User $user, bool $externalCall = false) { | ||||
|     parent::__construct($user, $externalCall, array( | ||||
|       "className" => new StringType("className", 64) | ||||
|     )); | ||||
|     $this->loginRequired = true; | ||||
|     $this->csrfTokenRequired = false; | ||||
|   } | ||||
| 
 | ||||
|   public function execute($values = array()) { | ||||
|     if (!parent::execute($values)) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     $className = $this->getParam("className"); | ||||
|     $fullClassName = "\\Configuration\\Patch\\" . $className; | ||||
|     $path = getClassPath($fullClassName, true); | ||||
|     if (!file_exists($path)) { | ||||
|       return $this->createError("File not found"); | ||||
|     } | ||||
| 
 | ||||
|     if(!class_exists($fullClassName)) { | ||||
|       return $this->createError("Class not found."); | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       $reflection = new \ReflectionClass($fullClassName); | ||||
|       if (!$reflection->isInstantiable()) { | ||||
|         return $this->createError("Class is not instantiable"); | ||||
|       } | ||||
| 
 | ||||
|       if (!$reflection->isSubclassOf(DatabaseScript::class)) { | ||||
|         return $this->createError("Not a database script."); | ||||
|       } | ||||
| 
 | ||||
|       $sql = $this->user->getSQL(); | ||||
|       $obj = $reflection->newInstance(); | ||||
|       $queries = $obj->createQueries($sql); | ||||
|       if (!is_array($queries)) { | ||||
|         return $this->createError("Database script returned invalid values"); | ||||
|       } | ||||
| 
 | ||||
|       foreach($queries as $query) { | ||||
|         if (!$query->execute()) { | ||||
|           return $this->createError("Query error: " . $sql->getLastError()); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       $this->success = true; | ||||
|     } catch (\ReflectionException $e) { | ||||
|       return $this->createError("Error reflecting class: " . $e->getMessage()); | ||||
|     } | ||||
| 
 | ||||
|     return $this->success; | ||||
|   } | ||||
| } | ||||
| @ -50,7 +50,7 @@ class Request { | ||||
|     foreach($this->params as $name => $param) { | ||||
|       $value = $values[$name] ?? NULL; | ||||
| 
 | ||||
|       $isEmpty = (is_string($value) || is_array($value)) && empty($value); | ||||
|       $isEmpty = (is_string($value) && strlen($value) === 0) || (is_array($value) && empty($value)); | ||||
|       if(!$param->optional && (is_null($value) || $isEmpty)) { | ||||
|         return $this->createError("Missing parameter: $name"); | ||||
|       } | ||||
| @ -187,6 +187,7 @@ class Request { | ||||
|   public function success() { return $this->success; } | ||||
|   public function loginRequired() { return $this->loginRequired; } | ||||
|   public function isExternalCall() { return $this->externalCall; } | ||||
|   public function clearError() { $this->success = true; $this->lastError = ""; } | ||||
| 
 | ||||
|   private function getMethod() { | ||||
|     $class = str_replace("\\", "/", get_class($this)); | ||||
|  | ||||
| @ -39,7 +39,7 @@ namespace Api\Settings { | ||||
| 
 | ||||
|        $query = $sql->select("name", "value") ->from("Settings"); | ||||
| 
 | ||||
|        if (!is_null($key) && !empty($key)) { | ||||
|        if (!is_null($key)) { | ||||
|          $query->where(new CondRegex(new Column("name"), $key)); | ||||
|        } | ||||
| 
 | ||||
|  | ||||
| @ -14,9 +14,38 @@ namespace Api\Visitors { | ||||
|   use Api\VisitorsAPI; | ||||
|   use DateTime; | ||||
|   use Driver\SQL\Condition\Compare; | ||||
|   use Driver\SQL\Expression\Add; | ||||
|   use Driver\SQL\Query\Select; | ||||
|   use Driver\SQL\Strategy\UpdateStrategy; | ||||
|   use Objects\User; | ||||
| 
 | ||||
|   class ProcessVisit extends VisitorsAPI { | ||||
|     public function __construct(User $user, bool $externalCall = false) { | ||||
|       parent::__construct($user, $externalCall, array( | ||||
|         "cookie" => new StringType("cookie") | ||||
|       )); | ||||
|       $this->isPublic = false; | ||||
|     } | ||||
| 
 | ||||
|     public function execute($values = array()) { | ||||
|       if (!parent::execute($values)) { | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|       $sql = $this->user->getSQL(); | ||||
|       $cookie = $this->getParam("cookie"); | ||||
|       $day = (new DateTime())->format("Ymd"); | ||||
|       $sql->insert("Visitor", array("cookie", "day")) | ||||
|         ->addRow($cookie, $day) | ||||
|         ->onDuplicateKeyStrategy(new UpdateStrategy( | ||||
|           array("day", "cookie"), | ||||
|           array("count" => new Add("Visitor.count", 1)))) | ||||
|         ->execute(); | ||||
| 
 | ||||
|       return $this->success; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   class Stats extends VisitorsAPI { | ||||
|     public function __construct(User $user, bool $externalCall = false) { | ||||
|       parent::__construct($user, $externalCall, array( | ||||
|  | ||||
| @ -6,7 +6,7 @@ use Driver\SQL\SQL; | ||||
| use \Driver\SQL\Strategy\SetNullStrategy; | ||||
| use \Driver\SQL\Strategy\CascadeStrategy; | ||||
| 
 | ||||
| class CreateDatabase { | ||||
| class CreateDatabase extends DatabaseScript { | ||||
| 
 | ||||
|   // NOTE:
 | ||||
|   // explicit serial ids removed due to postgres' serial implementation
 | ||||
| @ -192,7 +192,10 @@ class CreateDatabase { | ||||
|       ->addRow("User/edit", array(USER_GROUP_ADMIN), "Allows users to edit details and group memberships of any user") | ||||
|       ->addRow("User/delete", array(USER_GROUP_ADMIN), "Allows users to delete any other user") | ||||
|       ->addRow("Permission/fetch", array(USER_GROUP_ADMIN), "Allows users to list all API permissions") | ||||
|       ->addRow("Visitors/stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see visitor statistics"); | ||||
|       ->addRow("Visitors/stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see visitor statistics") | ||||
|       ->addRow("PatchSQL", array(USER_GROUP_ADMIN), "Allows users to import database patches"); | ||||
| 
 | ||||
|     self::loadPatches($queries, $sql); | ||||
| 
 | ||||
|     return $queries; | ||||
|   } | ||||
| @ -225,4 +228,22 @@ class CreateDatabase { | ||||
|       "Best Regards<br>" . | ||||
|       "{{site_name}} Administration"; | ||||
|   } | ||||
| 
 | ||||
|   private static function loadPatches(&$queries, $sql) { | ||||
|     $patchDirectory = './core/Configuration/Patch/'; | ||||
|     if (file_exists($patchDirectory) && is_dir($patchDirectory)) { | ||||
|       $scan_arr = scandir($patchDirectory); | ||||
|       $files_arr = array_diff($scan_arr, array('.','..')); | ||||
|       foreach ($files_arr as $file) { | ||||
|         $suffix = ".class.php"; | ||||
|         if (endsWith($file, $suffix)) { | ||||
|           $className = substr($file, 0, strlen($file) - strlen($suffix)); | ||||
|           $className = "\\Configuration\\Patch\\$className"; | ||||
|           $method = "$className::createQueries"; | ||||
|           $patchQueries = call_user_func($method, $sql); | ||||
|           foreach($patchQueries as $query) $queries[] = $query; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										9
									
								
								core/Configuration/DatabaseScript.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										9
									
								
								core/Configuration/DatabaseScript.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| <?php | ||||
| 
 | ||||
| namespace Configuration; | ||||
| 
 | ||||
| use Driver\SQL\SQL; | ||||
| 
 | ||||
| abstract class DatabaseScript { | ||||
|   public static abstract function createQueries(SQL $sql); | ||||
| } | ||||
							
								
								
									
										75
									
								
								core/Configuration/Patch/file_api.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										75
									
								
								core/Configuration/Patch/file_api.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | ||||
| <?php | ||||
| 
 | ||||
| namespace Configuration\Patch; | ||||
| 
 | ||||
| use Configuration\DatabaseScript; | ||||
| use Driver\SQL\SQL; | ||||
| use Driver\SQL\Column\Column; | ||||
| use Driver\SQL\Strategy\CascadeStrategy; | ||||
| use Driver\SQL\Strategy\UpdateStrategy; | ||||
| 
 | ||||
| class file_api extends DatabaseScript { | ||||
| 
 | ||||
|   public static function createQueries(SQL $sql) { | ||||
| 
 | ||||
|     $queries = array(); | ||||
| 
 | ||||
|     $queries[] = $sql->insert("ApiPermission", array("method", "groups", "description")) | ||||
|       ->onDuplicateKeyStrategy(new UpdateStrategy(array("method"), array("method" => new Column("method")))) | ||||
|       ->addRow("File/GetRestrictions", array(), "Allows users to view global upload restrictions") | ||||
|       ->addRow("File/Download", array(), "Allows users to download files when logged in, or using a given token") | ||||
|       ->addRow("File/Upload", array(), "Allows users to upload files when logged in, or using a given token") | ||||
|       ->addRow("File/ValidateToken", array(), "Allows users to validate a given token") | ||||
|       ->addRow("File/RevokeToken", array(USER_GROUP_ADMIN), "Allows users to revoke a token") | ||||
|       ->addRow("File/ListFiles", array(), "Allows users to list all files assigned to an account") | ||||
|       ->addRow("File/ListTokens", array(USER_GROUP_ADMIN), "Allows users to list all tokens assigned to the virtual filesystem of an account") | ||||
|       ->addRow("File/CreateDirectory", array(), "Allows users to create a virtual directory") | ||||
|       ->addRow("File/Rename", array(), "Allows users to rename files in the virtual filesystem") | ||||
|       ->addRow("File/Move", array(), "Allows users to move files in the virtual filesystem") | ||||
|       ->addRow("File/Delete", array(), "Allows users to delete files in the virtual filesystem") | ||||
|       ->addRow("File/CreateUploadToken", array(USER_GROUP_ADMIN), "Allows users to create a token to upload files to the virtual filesystem assigned to the users account") | ||||
|       ->addRow("File/CreateDownloadToken", array(USER_GROUP_ADMIN), "Allows users to create a token to download files from the virtual filesystem assigned to the users account"); | ||||
| 
 | ||||
|     $queries[] = $sql->insert("Route", array("request", "action", "target", "extra")) | ||||
|       ->onDuplicateKeyStrategy(new UpdateStrategy(array("request"), array("request" => new Column("request")))) | ||||
|       ->addRow("^/files(/.*)?$", "dynamic", "\\Documents\\Files", NULL); | ||||
| 
 | ||||
|     $queries[] = $sql->createTable("UserFile") | ||||
|       ->onlyIfNotExists() | ||||
|       ->addSerial("uid") | ||||
|       ->addBool("directory") | ||||
|       ->addString("name", 64, false) | ||||
|       ->addString("path", 512, true) | ||||
|       ->addInt("parent_id", true) | ||||
|       ->addInt("user_id") | ||||
|       ->primaryKey("uid") | ||||
|       ->unique("user_id", "parent_id", "name") | ||||
|       ->foreignKey("parent_id", "UserFile", "uid", new CascadeStrategy()) | ||||
|       ->foreignKey("user_id", "User", "uid", new CascadeStrategy()); | ||||
| 
 | ||||
|     $queries[] = $sql->createTable("UserFileToken") | ||||
|       ->onlyIfNotExists() | ||||
|       ->addSerial("uid") | ||||
|       ->addString("token", 36, false) | ||||
|       ->addDateTime("valid_until", true) | ||||
|       ->addEnum("token_type", array("download", "upload")) | ||||
|       ->addInt("user_id") | ||||
|       # upload only:
 | ||||
|       ->addInt("maxFiles", true) | ||||
|       ->addInt("maxSize", true) | ||||
|       ->addInt("parent_id", true) | ||||
|       ->addString("extensions", 64, true) | ||||
|       ->primaryKey("uid") | ||||
|       ->foreignKey("user_id", "User", "uid", new CascadeStrategy()) | ||||
|       ->foreignKey("parent_id", "UserFile", "uid", new CascadeStrategy()); | ||||
| 
 | ||||
|     $queries[] = $sql->createTable("UserFileTokenFile") | ||||
|       ->addInt("file_id") | ||||
|       ->addInt("token_id") | ||||
|       ->unique("file_id", "token_id") | ||||
|       ->foreignKey("file_id", "UserFile", "uid", new CascadeStrategy()) | ||||
|       ->foreignKey("token_id", "UserFileToken", "uid", new CascadeStrategy()); | ||||
| 
 | ||||
|     return $queries; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										64
									
								
								core/Documents/Files.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										64
									
								
								core/Documents/Files.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| <?php | ||||
| 
 | ||||
| namespace Documents { | ||||
| 
 | ||||
|   use Documents\Files\FilesBody; | ||||
|   use Documents\Files\FilesHead; | ||||
|   use Elements\Document; | ||||
|   use Objects\User; | ||||
| 
 | ||||
|   class Files extends Document { | ||||
|     public function __construct(User $user, string $view = NULL) { | ||||
|       parent::__construct($user, FilesHead::class, FilesBody::class, $view); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| namespace Documents\Files { | ||||
| 
 | ||||
|   use Elements\Head; | ||||
|   use Elements\Script; | ||||
|   use Elements\SimpleBody; | ||||
| 
 | ||||
|   class FilesHead extends Head { | ||||
| 
 | ||||
|     protected function initSources() { | ||||
|       $this->loadBootstrap(); | ||||
|       $this->loadFontawesome(); | ||||
|     } | ||||
| 
 | ||||
|     protected function initMetas() { | ||||
|       return array( | ||||
|         array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0'), | ||||
|         array('name' => 'format-detection', 'content' => 'telephone=yes'), | ||||
|         array('charset' => 'utf-8'), | ||||
|         array('http-equiv' => 'expires', 'content' => '0'), | ||||
|         array('name' => 'robots', 'content' => 'noarchive'), | ||||
|         array('name' => 'referrer', 'content' => 'origin') | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     protected function initRawFields() { | ||||
|       return array(); | ||||
|     } | ||||
| 
 | ||||
|     protected function initTitle() { | ||||
|       return "File Control Panel"; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   class FilesBody extends SimpleBody { | ||||
| 
 | ||||
|     public function __construct($document) { | ||||
|       parent::__construct($document); | ||||
|     } | ||||
| 
 | ||||
|     protected function getContent() { | ||||
|       $html = "<noscript>" . $this->createErrorText("Javascript is required for this site to render.") . "</noscript>"; | ||||
|       $html .= "<div id=\"root\"></div>"; | ||||
|       $html .= new Script(Script::MIME_TEXT_JAVASCRIPT, Script::FILES); | ||||
|       return $html; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| @ -234,7 +234,7 @@ namespace Documents\Install { | ||||
|         $missingInputs[] = "Type"; | ||||
|       } | ||||
| 
 | ||||
|       $supportedTypes = array("mysql", "postgres"); # , "oracle", "postgres");
 | ||||
|       $supportedTypes = array("mysql", "postgres"); | ||||
|       if(!$success) { | ||||
|         $msg = "Please fill out the following inputs:<br>" . | ||||
|           $this->createUnorderedList($missingInputs); | ||||
| @ -590,7 +590,7 @@ namespace Documents\Install { | ||||
|           "title" => "Database configuration", | ||||
|           "form" => array( | ||||
|             array("title" => "Database Type", "name" => "type", "type" => "select", "required" => true, "items" => array( | ||||
|               "mysql" => "MySQL", "oracle" => "Oracle", "postgres" => "PostgreSQL" | ||||
|               "mysql" => "MySQL", "postgres" => "PostgreSQL" | ||||
|             )), | ||||
|             array("title" => "Username", "name"  => "username", "type"  => "text", "required" => true), | ||||
|             array("title" => "Password", "name"  => "password", "type"  => "password"), | ||||
|  | ||||
							
								
								
									
										14
									
								
								core/Driver/SQL/Condition/CondNull.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										14
									
								
								core/Driver/SQL/Condition/CondNull.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| <?php | ||||
| 
 | ||||
| namespace Driver\SQL\Condition; | ||||
| 
 | ||||
| class CondNull extends Condition { | ||||
| 
 | ||||
|   private string $column; | ||||
| 
 | ||||
|   public function __construct(string $col) { | ||||
|     $this->column = $col; | ||||
|   } | ||||
| 
 | ||||
|   public function getColumn() { return $this->column; } | ||||
| } | ||||
| @ -8,17 +8,20 @@ class Join { | ||||
|   private string $table; | ||||
|   private string $columnA; | ||||
|   private string $columnB; | ||||
|   private $tableAlias; | ||||
| 
 | ||||
|   public function __construct($type, $table, $columnA, $columnB) { | ||||
|   public function __construct($type, $table, $columnA, $columnB, $tableAlias=null) { | ||||
|     $this->type = $type; | ||||
|     $this->table = $table; | ||||
|     $this->columnA = $columnA; | ||||
|     $this->columnB = $columnB; | ||||
|     $this->tableAlias = $tableAlias; | ||||
|   } | ||||
| 
 | ||||
|   public function getType() { return $this->type; } | ||||
|   public function getTable() { return $this->table; } | ||||
|   public function getColumnA() { return $this->columnA; } | ||||
|   public function getColumnB() { return $this->columnB; } | ||||
|   public function getTableAlias() { return $this->tableAlias; } | ||||
| 
 | ||||
| } | ||||
| @ -4,6 +4,7 @@ namespace Driver\SQL; | ||||
| 
 | ||||
| use \Api\Parameter\Parameter; | ||||
| 
 | ||||
| use DateTime; | ||||
| use \Driver\SQL\Column\Column; | ||||
| use \Driver\SQL\Column\IntColumn; | ||||
| use \Driver\SQL\Column\SerialColumn; | ||||
| @ -91,15 +92,21 @@ class MySQL extends SQL { | ||||
|           $sqlParams[0] .= 'd'; | ||||
|           break; | ||||
|         case Parameter::TYPE_DATE: | ||||
|           if ($value instanceof DateTime) { | ||||
|             $value = $value->format('Y-m-d'); | ||||
|           } | ||||
|           $sqlParams[0] .= 's'; | ||||
|           break; | ||||
|         case Parameter::TYPE_TIME: | ||||
|           if ($value instanceof DateTime) { | ||||
|             $value = $value->format('H:i:s'); | ||||
|           } | ||||
|           $sqlParams[0] .= 's'; | ||||
|           break; | ||||
|         case Parameter::TYPE_DATE_TIME: | ||||
|           if ($value instanceof DateTime) { | ||||
|             $value = $value->format('Y-m-d H:i:s'); | ||||
|           } | ||||
|           $sqlParams[0] .= 's'; | ||||
|           break; | ||||
|         case Parameter::TYPE_ARRAY: | ||||
|  | ||||
							
								
								
									
										29
									
								
								core/Driver/SQL/Query/Drop.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										29
									
								
								core/Driver/SQL/Query/Drop.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| <?php | ||||
| 
 | ||||
| 
 | ||||
| namespace Driver\SQL\Query; | ||||
| 
 | ||||
| use Driver\SQL\SQL; | ||||
| 
 | ||||
| class Drop extends Query { | ||||
| 
 | ||||
|   private string $table; | ||||
| 
 | ||||
|   /** | ||||
|    * Drop constructor. | ||||
|    * @param SQL $sql | ||||
|    * @param string $table | ||||
|    */ | ||||
|   public function __construct(\Driver\SQL\SQL $sql, string $table) { | ||||
|     parent::__construct($sql); | ||||
|     $this->table = $table; | ||||
|   } | ||||
| 
 | ||||
|   public function execute() { | ||||
|     $this->sql->executeDrop($this); | ||||
|   } | ||||
| 
 | ||||
|   public function getTable() { | ||||
|     return $this->table; | ||||
|   } | ||||
| } | ||||
| @ -40,13 +40,13 @@ class Select extends Query { | ||||
|     return $this; | ||||
|   } | ||||
| 
 | ||||
|   public function innerJoin($table, $columnA, $columnB) { | ||||
|     $this->joins[] = new Join("INNER", $table, $columnA, $columnB); | ||||
|   public function innerJoin($table, $columnA, $columnB, $tableAlias=null) { | ||||
|     $this->joins[] = new Join("INNER", $table, $columnA, $columnB, $tableAlias); | ||||
|     return $this; | ||||
|   } | ||||
| 
 | ||||
|   public function leftJoin($table, $columnA, $columnB) { | ||||
|     $this->joins[] = new Join("LEFT", $table, $columnA, $columnB); | ||||
|   public function leftJoin($table, $columnA, $columnB, $tableAlias=null) { | ||||
|     $this->joins[] = new Join("LEFT", $table, $columnA, $columnB, $tableAlias); | ||||
|     return $this; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -9,6 +9,7 @@ use Driver\SQL\Condition\CondIn; | ||||
| use Driver\SQL\Condition\Condition; | ||||
| use Driver\SQL\Condition\CondKeyword; | ||||
| use Driver\SQL\Condition\CondNot; | ||||
| use Driver\Sql\Condition\CondNull; | ||||
| use Driver\SQL\Condition\CondOr; | ||||
| use Driver\SQL\Constraint\Constraint; | ||||
| use \Driver\SQL\Constraint\Unique; | ||||
| @ -16,6 +17,7 @@ use \Driver\SQL\Constraint\PrimaryKey; | ||||
| use \Driver\SQL\Constraint\ForeignKey; | ||||
| use Driver\SQL\Query\CreateTable; | ||||
| use Driver\SQL\Query\Delete; | ||||
| use Driver\SQL\Query\Drop; | ||||
| use Driver\SQL\Query\Insert; | ||||
| use Driver\SQL\Query\Query; | ||||
| use Driver\SQL\Query\Select; | ||||
| @ -73,6 +75,10 @@ abstract class SQL { | ||||
|     return new Update($this, $table); | ||||
|   } | ||||
| 
 | ||||
|   public function drop(string $table) { | ||||
|     return new Drop($this, $table); | ||||
|   } | ||||
| 
 | ||||
|   // ####################
 | ||||
|   // ### ABSTRACT METHODS
 | ||||
|   // ####################
 | ||||
| @ -107,7 +113,9 @@ abstract class SQL { | ||||
|           $joinTable = $this->tableName($join->getTable()); | ||||
|           $columnA = $this->columnName($join->getColumnA()); | ||||
|           $columnB = $this->columnName($join->getColumnB()); | ||||
|           $joinStr .= " $type JOIN $joinTable ON $columnA=$columnB"; | ||||
|           $tableAlias = ($join->getTableAlias() ? " " . $join->getTableAlias() : ""); | ||||
| 
 | ||||
|           $joinStr .= " $type JOIN $joinTable$tableAlias ON $columnA=$columnB"; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
| @ -248,6 +256,12 @@ abstract class SQL { | ||||
|     return $this->execute($query, $params); | ||||
|   } | ||||
| 
 | ||||
|   public function executeDrop(Drop $drop) { | ||||
|     $query = "DROP TABLE " . $this->tableName($drop->getTable()); | ||||
|     if ($drop->dump) { var_dump($query); } | ||||
|     return $this->execute($query); | ||||
|   } | ||||
| 
 | ||||
|   protected function getWhereClause($conditions, &$params) { | ||||
|     if (!$conditions) { | ||||
|       return ""; | ||||
| @ -338,6 +352,15 @@ abstract class SQL { | ||||
|       $column = $this->columnName($condition->getColumn()); | ||||
|       $value = $condition->getValue(); | ||||
|       $operator = $condition->getOperator(); | ||||
| 
 | ||||
|       if ($value === null) { | ||||
|         if ($operator === "=") { | ||||
|           return "$column IS NULL"; | ||||
|         } else if ($operator === "!=") { | ||||
|           return "$column IS NOT NULL"; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return $column . $operator . $this->addValue($value, $params); | ||||
|     } else if ($condition instanceof CondBool) { | ||||
|       return $this->columnName($condition->getValue()); | ||||
| @ -385,6 +408,8 @@ abstract class SQL { | ||||
|       } | ||||
| 
 | ||||
|       return "NOT $expression"; | ||||
|     } else if($condition instanceof CondNull) { | ||||
|       return $this->columnName($condition->getColumn()) . " IS NULL"; | ||||
|     } else { | ||||
|       $this->lastError = "Unsupported condition type: " . get_class($condition); | ||||
|       return false; | ||||
| @ -410,9 +435,6 @@ abstract class SQL { | ||||
|       $sql = new MySQL($connectionData); | ||||
|     } else if ($type === "postgres") { | ||||
|       $sql = new PostgreSQL($connectionData); | ||||
|     /*} else if ($type === "oracle") { | ||||
|       // $sql = new OracleSQL($connectionData);
 | ||||
|     */ | ||||
|     } else { | ||||
|       return "Unknown database type"; | ||||
|     } | ||||
|  | ||||
							
								
								
									
										7
									
								
								core/Elements/EmptyBody.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										7
									
								
								core/Elements/EmptyBody.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| <?php | ||||
| 
 | ||||
| use Elements\Body; | ||||
| 
 | ||||
| class EmptyBody extends Body { | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										26
									
								
								core/Elements/EmptyHead.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										26
									
								
								core/Elements/EmptyHead.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| <?php | ||||
| 
 | ||||
| namespace Elements; | ||||
| 
 | ||||
| class EmptyHead extends Head { | ||||
| 
 | ||||
|   public function __construct($document) { | ||||
|     parent::__construct($document); | ||||
|   } | ||||
| 
 | ||||
|   protected function initSources() { | ||||
|   } | ||||
| 
 | ||||
|   protected function initMetas() { | ||||
|     return array( | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   protected function initRawFields() { | ||||
|     return array(); | ||||
|   } | ||||
| 
 | ||||
|   protected function initTitle() { | ||||
|     return ""; | ||||
|   } | ||||
| } | ||||
| @ -11,6 +11,7 @@ class Script extends StaticView { | ||||
|   const INSTALL   = "/js/install.js"; | ||||
|   const BOOTSTRAP = "/js/bootstrap.bundle.min.js"; | ||||
|   const ACCOUNT   = "/js/account.js"; | ||||
|   const FILES     = "/js/files.min.js"; | ||||
| 
 | ||||
|   private string $type; | ||||
|   private string $content; | ||||
|  | ||||
| @ -2,8 +2,6 @@ | ||||
| 
 | ||||
| namespace Elements; | ||||
| 
 | ||||
| use External\PHPMailer\Exception; | ||||
| 
 | ||||
| abstract class View extends StaticView { | ||||
| 
 | ||||
|   private Document $document; | ||||
| @ -81,9 +79,10 @@ abstract class View extends StaticView { | ||||
|     return $this->createList($items, "ul"); | ||||
|   } | ||||
| 
 | ||||
|   protected function createLink($link, $title=null) { | ||||
|   protected function createLink($link, $title=null, $classes="") { | ||||
|     if(is_null($title)) $title=$link; | ||||
|     return "<a href=\"$link\">$title</a>";
 | ||||
|     if(!empty($classes)) $classes = " class=\"$classes\"";
 | ||||
|     return "<a href=\"$link\"$classes>$title</a>";
 | ||||
|   } | ||||
| 
 | ||||
|   protected function createExternalLink($link, $title=null) { | ||||
| @ -123,14 +122,91 @@ abstract class View extends StaticView { | ||||
|     return $this->createStatusText("info", $text, $id, $hidden); | ||||
|   } | ||||
| 
 | ||||
|   protected function createStatusText($type, $text, $id="", $hidden=false) { | ||||
|   protected function createStatusText($type, $text, $id="", $hidden=false, $classes="") { | ||||
|     if(strlen($id) > 0) $id = " id=\"$id\"";
 | ||||
|     $hidden = ($hidden?" hidden" : ""); | ||||
|     return "<div class=\"alert alert-$type$hidden\" role=\"alert\"$id>$text</div>"; | ||||
|     if($hidden) $classes .= " hidden"; | ||||
|     if(strlen($classes) > 0) $classes = " $classes"; | ||||
|     return "<div class=\"alert alert-$type$hidden$classes\" role=\"alert\"$id>$text</div>"; | ||||
|   } | ||||
| 
 | ||||
|   protected function createBadge($type, $text) { | ||||
|     $text = htmlspecialchars($text); | ||||
|     return "<span class=\"badge badge-$type\">$text</span>";
 | ||||
|   } | ||||
| 
 | ||||
|   protected function createJumbotron(string $content, bool $fluid=false, $class="") { | ||||
|     $jumbotronClass = "jumbotron" . ($fluid ? "-fluid" : ""); | ||||
|     if (!empty($class)) $jumbotronClass .= " $class"; | ||||
| 
 | ||||
|     return " | ||||
|       <div class=\"row\">
 | ||||
|         <div class=\"col-12\">
 | ||||
|           <div class=\"$jumbotronClass\"> | ||||
|             $content | ||||
|           </div> | ||||
|         </div> | ||||
|       </div>";
 | ||||
|   } | ||||
| 
 | ||||
|   public function createSimpleParagraph(string $content, string $class="") { | ||||
|     if($class) $class = " class=\"$class\"";
 | ||||
|     return "<p$class>$content</p>"; | ||||
|   } | ||||
| 
 | ||||
|   public function createParagraph($title, $id, $content) { | ||||
|     $id = replaceCssSelector($id); | ||||
|     $iconId = urlencode("$id-icon"); | ||||
|     return " | ||||
|       <div class=\"row mt-4\">
 | ||||
|         <div class=\"col-12\">
 | ||||
|           <h2 id=\"$id\" data-target=\"$iconId\" class=\"inlineLink\">$title</h2>
 | ||||
|           <hr/> | ||||
|           $content | ||||
|         </div> | ||||
|       </div>";
 | ||||
|   } | ||||
| 
 | ||||
|   protected function createBootstrapTable($data, string $classes="") { | ||||
|     $classes = empty($classes) ? "" : " $classes"; | ||||
|     $code = "<div class=\"container$classes\">";
 | ||||
|     foreach($data as $row) { | ||||
|       $code .= "<div class=\"row mt-2 mb-2\">"; | ||||
|       $columnCount = count($row); | ||||
|       if($columnCount > 0) { | ||||
|         $remainingSize = 12; | ||||
|         $columnSize = 12 / $columnCount; | ||||
|         foreach($row as $col) { | ||||
|           $size = ($columnSize <= $remainingSize ? $columnSize : $remainingSize); | ||||
|           $content = $col; | ||||
|           $class = ""; | ||||
|           $code .= "<div"; | ||||
| 
 | ||||
|           if(is_array($col)) { | ||||
|             $content = ""; | ||||
|             foreach($col as $key => $val) { | ||||
|               if(strcmp($key, "content") === 0) { | ||||
|                 $content = $val; | ||||
|               } else if(strcmp($key, "class") === 0) { | ||||
|                 $class = " " . $col["class"]; | ||||
|               } else if(strcmp($key, "cols") === 0 && is_numeric($val)) { | ||||
|                 $size = intval($val); | ||||
|               } else { | ||||
|                 $code .= " $key=\"$val\"";
 | ||||
|               } | ||||
|             } | ||||
| 
 | ||||
|             if(isset($col["class"])) $class = " " . $col["class"]; | ||||
|           } | ||||
| 
 | ||||
|           if($size <= 6) $class .= " col-md-" . intval($size * 2); | ||||
|           $code .= " class=\"col-lg-$size$class\">$content</div>";
 | ||||
|           $remainingSize -= $size; | ||||
|         } | ||||
|       } | ||||
|       $code .= "</div>"; | ||||
|     } | ||||
| 
 | ||||
|     $code .= "</div>"; | ||||
|     return $code; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										62
									
								
								core/External/ZipStream/BufferWriter.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										62
									
								
								core/External/ZipStream/BufferWriter.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| <?php | ||||
| 
 | ||||
| /** | ||||
|  * Copyright (c) Borago 2019 | ||||
|  * | ||||
|  * This software is provided 'as-is', without any express or implied | ||||
|  * warranty. In no event will the authors be held liable for any damages | ||||
|  * arising from the use of this software. | ||||
|  * | ||||
|  * Permission is granted to anyone to use this software for any purpose, | ||||
|  * including commercial applications, and to alter it and redistribute it | ||||
|  * freely, subject to the following restrictions: | ||||
|  * | ||||
|  * 1. The origin of this software must not be misrepresented; you must not | ||||
|  *    claim that you wrote the original software. If you use this software | ||||
|  *    in a product, an acknowledgment in the product documentation would be | ||||
|  *    appreciated but is not required. | ||||
|  * 2. Altered source versions must be plainly marked as such, and must not be | ||||
|  *    misrepresented as being the original software. | ||||
|  * 3. This notice may not be removed or altered from any source distribution. | ||||
|  **/ | ||||
| 
 | ||||
| namespace External\ZipStream { | ||||
|   class BufferWriter implements Writer { | ||||
|     private $stream = ''; | ||||
|     private $offset = 0; | ||||
|     private $callback = null; | ||||
| 
 | ||||
|     public function __construct() { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public function registerCallback($callback) { | ||||
|       $this->callback = $callback; | ||||
|     } | ||||
| 
 | ||||
|     public function write($data) { | ||||
|       $this->offset += strlen($data); | ||||
|       $this->stream .= $data; | ||||
|       if ($this->callback !== null) { | ||||
|         call_user_func($this->callback, $this); | ||||
|       } | ||||
|       return strlen($data); | ||||
|     } | ||||
| 
 | ||||
|     public function read() { | ||||
|       $data = $this->stream; | ||||
|       $this->stream = ''; | ||||
|       return $data; | ||||
|     } | ||||
| 
 | ||||
|     public function offset() { | ||||
|       return $this->offset; | ||||
|     } | ||||
| 
 | ||||
|     public function close() { | ||||
|       if ($this->callback !== null) { | ||||
|         call_user_func($this->callback, $this); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										207
									
								
								core/External/ZipStream/File.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										207
									
								
								core/External/ZipStream/File.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,207 @@ | ||||
| <?php | ||||
| 
 | ||||
| /** | ||||
|  * Copyright (c) Borago 2019 | ||||
|  * | ||||
|  * This software is provided 'as-is', without any express or implied | ||||
|  * warranty. In no event will the authors be held liable for any damages | ||||
|  * arising from the use of this software. | ||||
|  * | ||||
|  * Permission is granted to anyone to use this software for any purpose, | ||||
|  * including commercial applications, and to alter it and redistribute it | ||||
|  * freely, subject to the following restrictions: | ||||
|  * | ||||
|  * 1. The origin of this software must not be misrepresented; you must not | ||||
|  *    claim that you wrote the original software. If you use this software | ||||
|  *    in a product, an acknowledgment in the product documentation would be | ||||
|  *    appreciated but is not required. | ||||
|  * 2. Altered source versions must be plainly marked as such, and must not be | ||||
|  *    misrepresented as being the original software. | ||||
|  * 3. This notice may not be removed or altered from any source distribution. | ||||
|  **/ | ||||
| 
 | ||||
| namespace External\ZipStream { | ||||
|   class File { | ||||
|     private $name; | ||||
|     private $content = ''; | ||||
|     private $fileHandle = false; | ||||
|     private $lastModificationTimestamp; | ||||
|     private $crc32 = null; | ||||
|     private $fileSize = 0; | ||||
|     private $compressedSize = 0; | ||||
|     private $offset = 0; | ||||
|     private $bitField = 0; | ||||
|     private $useCompression = true; | ||||
|     private $deflateState = null; | ||||
| 
 | ||||
|     //check for duplications //currently not used
 | ||||
|     private $sha256; | ||||
| 
 | ||||
|     public const BIT_NO_SIZE_IN_HEADER = 0b0000000000001000; | ||||
|     public const BIT_UTF8_NAMES = 0b0000100000000000; | ||||
| 
 | ||||
|     public function __construct($name) { | ||||
|       $this->name = $name; | ||||
|       $this->lastModificationTimestamp = time(); | ||||
|       $this->crc32 = hash('crc32b', '', true); | ||||
|       $this->compressedSize = 0; | ||||
| 
 | ||||
|       $this->bitField = 0; | ||||
|       $this->bitField |= self::BIT_NO_SIZE_IN_HEADER; | ||||
|       $this->bitField |= self::BIT_UTF8_NAMES; | ||||
| 
 | ||||
|       $this->deflateState = deflate_init(ZLIB_ENCODING_RAW, ['level' => 9]); | ||||
|     } | ||||
| 
 | ||||
|     public function setContent($content) { | ||||
|       $this->crc32 = hash('crc32b', $content, true); | ||||
|       $this->sha256 = hash('sha256', $content); | ||||
|       $this->content = $content; | ||||
|       $this->fileSize = strlen($content); | ||||
|       $this->fileHandle = false; | ||||
|     } | ||||
| 
 | ||||
|     public function loadFromFile($filename) { | ||||
|       $this->crc32 = hash_file('crc32b', $filename, true); | ||||
|       $this->sha256 = hash_file('sha256', $filename); | ||||
|       $this->fileSize = filesize($filename); | ||||
|       $this->fileHandle = fopen($filename, 'rb'); | ||||
|     } | ||||
| 
 | ||||
|     public function name() { | ||||
|       return $this->name; | ||||
|     } | ||||
| 
 | ||||
|     public function sha256() { | ||||
|       return $this->sha256; | ||||
|     } | ||||
| 
 | ||||
|     private function unixTimeToDosTime($timestamp) { | ||||
|       $hour = intval(date('H', $timestamp)); | ||||
|       $min = intval(date('i', $timestamp)); | ||||
|       $sec = intval(date('s', $timestamp)); | ||||
|       return ($hour << 11) | | ||||
|         ($min << 5) | | ||||
|         ($sec >> 1); | ||||
|     } | ||||
| 
 | ||||
|     private function unixTimeToDosDate($timestamp) { | ||||
|       $year = intval(date('Y', $timestamp)); | ||||
|       $month = intval(date('m', $timestamp)); | ||||
|       $day = intval(date('d', $timestamp)); | ||||
|       return (($year - 1980) << 9) | | ||||
|         ($month << 5) | | ||||
|         ($day); | ||||
|     } | ||||
| 
 | ||||
|     public function readLocalFileHeader() { | ||||
|       if (!$this->useCompression) { | ||||
|         $this->compressedSize = $this->fileSize; | ||||
|       } | ||||
| 
 | ||||
|       $header = ""; | ||||
|       $header .= "\x50\x4b\x03\x04"; | ||||
|       $header .= "\x14\x00"; //version 2.0 and MS-DOS compatible
 | ||||
|       $header .= pack("v", $this->bitField); //general purpose bit flag
 | ||||
|       if ($this->useCompression) { | ||||
|         $header .= "\x08\x00"; //compression Method - deflate
 | ||||
|       } else { | ||||
|         $header .= "\x00\x00"; //compression Method - no
 | ||||
|       } | ||||
|       $header .= pack("v", $this->unixTimeToDosTime($this->lastModificationTimestamp)); //dos time
 | ||||
|       $header .= pack("v", $this->unixTimeToDosDate($this->lastModificationTimestamp)); //dos date
 | ||||
|       if ($this->bitField & self::BIT_NO_SIZE_IN_HEADER) { | ||||
|         $header .= pack("V", 0); //crc32
 | ||||
|         $header .= pack("V", 0); //compressed Size
 | ||||
|         $header .= pack("V", 0); //uncompressed Size
 | ||||
|       } else { | ||||
|         $header .= strrev($this->crc32); | ||||
|         $header .= pack("V", $this->compressedSize); //compressed Size
 | ||||
|         $header .= pack("V", $this->fileSize); //uncompressed Size
 | ||||
|       } | ||||
|       $header .= pack("v", strlen($this->name)); //filename
 | ||||
|       $header .= "\x00\x00"; //extra field length
 | ||||
|       $header .= $this->name; | ||||
| 
 | ||||
|       return $header; | ||||
|     } | ||||
| 
 | ||||
|     public function readDataDescriptor() { | ||||
|       $data = ""; | ||||
|       $data .= "\x50\x4b\x07\x08"; | ||||
|       $data .= strrev($this->crc32); | ||||
|       $data .= pack("V", $this->compressedSize); //compressed Size
 | ||||
|       $data .= pack("V", $this->fileSize); //uncompressed Size
 | ||||
|       return $data; | ||||
|     } | ||||
| 
 | ||||
|     public function readFileDataImp() { | ||||
|       $ret = null; | ||||
|       if ($this->fileHandle !== false) { | ||||
|         $block = fread($this->fileHandle, 65536); | ||||
|         if (!empty($block)) { | ||||
|           $ret = $block; | ||||
|         } | ||||
|       } else { | ||||
|         $ret = $this->content; | ||||
|         $this->content = null; | ||||
|       } | ||||
|       return $ret; | ||||
|     } | ||||
| 
 | ||||
|     public function readFileData() { | ||||
|       $ret = null; | ||||
|       if ($this->useCompression) { | ||||
|         $block = $this->readFileDataImp(); | ||||
|         if ($this->deflateState !== null) { | ||||
|           if ($block !== null) { | ||||
|             $ret = deflate_add($this->deflateState, $block, ZLIB_NO_FLUSH); | ||||
|           } else { | ||||
|             $ret = deflate_add($this->deflateState, '', ZLIB_FINISH); | ||||
|             $this->deflateState = null; | ||||
|           } | ||||
|         } | ||||
|         if ($ret !== null) { | ||||
|           $this->compressedSize += strlen($ret); | ||||
|         } | ||||
|       } else { | ||||
|         $ret = $this->readFileDataImp(); | ||||
|       } | ||||
|       return $ret; | ||||
|     } | ||||
| 
 | ||||
|     public function setOffset($offset) { | ||||
|       $this->offset = $offset; | ||||
|     } | ||||
| 
 | ||||
|     public function readCentralDirectoryHeader() { | ||||
|       $header = ""; | ||||
|       $header .= "\x50\x4b\x01\x02"; | ||||
|       $header .= "\x14\x00"; //version 2.0 and MS-DOS compatible
 | ||||
|       $header .= "\x14\x00"; //version 2.0 and MS-DOS compatible
 | ||||
|       $header .= pack("v", $this->bitField); //general purpose bit flag
 | ||||
|       $header .= "\x00\x00"; //compression Method - no
 | ||||
|       $header .= pack("v", $this->unixTimeToDosTime($this->lastModificationTimestamp)); //dos time
 | ||||
|       $header .= pack("v", $this->unixTimeToDosDate($this->lastModificationTimestamp)); //dos date
 | ||||
|       $header .= strrev($this->crc32); | ||||
|       $header .= pack("V", $this->compressedSize); //compressed Size
 | ||||
|       $header .= pack("V", $this->fileSize); //uncompressed Size
 | ||||
|       $header .= pack("v", strlen($this->name)); //filename
 | ||||
|       $header .= "\x00\x00"; //extra field length
 | ||||
|       $header .= "\x00\x00"; //comment length
 | ||||
|       $header .= "\x00\x00"; //disk num start
 | ||||
|       $header .= "\x00\x00"; //int file attr
 | ||||
|       $header .= "\x00\x00\x00\x00"; //ext file attr
 | ||||
|       $header .= pack("V", $this->offset); //relative offset
 | ||||
|       $header .= $this->name; | ||||
| 
 | ||||
|       return $header; | ||||
|     } | ||||
| 
 | ||||
|     public function closeHandle() { | ||||
|       if ($this->fileHandle) { | ||||
|         fclose($this->fileHandle); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										63
									
								
								core/External/ZipStream/FileWriter.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										63
									
								
								core/External/ZipStream/FileWriter.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| <?php | ||||
| 
 | ||||
| /** | ||||
|  * Copyright (c) Borago 2019 | ||||
|  * | ||||
|  * This software is provided 'as-is', without any express or implied | ||||
|  * warranty. In no event will the authors be held liable for any damages | ||||
|  * arising from the use of this software. | ||||
|  * | ||||
|  * Permission is granted to anyone to use this software for any purpose, | ||||
|  * including commercial applications, and to alter it and redistribute it | ||||
|  * freely, subject to the following restrictions: | ||||
|  * | ||||
|  * 1. The origin of this software must not be misrepresented; you must not | ||||
|  *    claim that you wrote the original software. If you use this software | ||||
|  *    in a product, an acknowledgment in the product documentation would be | ||||
|  *    appreciated but is not required. | ||||
|  * 2. Altered source versions must be plainly marked as such, and must not be | ||||
|  *    misrepresented as being the original software. | ||||
|  * 3. This notice may not be removed or altered from any source distribution. | ||||
|  **/ | ||||
| 
 | ||||
| namespace External\ZipStream { | ||||
|   class FileWriter implements Writer { | ||||
|     private $offset = 0; | ||||
|     private $fileHandle = false; | ||||
| 
 | ||||
|     public function __construct($filename) { | ||||
|       if (!empty($filename)) { | ||||
|         $this->open($filename); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     public function __destruct() { | ||||
|       $this->close(); | ||||
|     } | ||||
| 
 | ||||
|     public function open($filename) { | ||||
|       $this->close(); | ||||
|       $this->fileHandle = fopen($filename, 'wb'); | ||||
|     } | ||||
| 
 | ||||
|     public function write($data) { | ||||
|       $this->offset += strlen($data); | ||||
|       if ($this->fileHandle === false) { | ||||
|         throw new \Exception('No file opened.'); | ||||
|       } else { | ||||
|         return fwrite($this->fileHandle, $data); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     public function offset() { | ||||
|       return $this->offset; | ||||
|     } | ||||
| 
 | ||||
|     public function close() { | ||||
|       if ($this->fileHandle !== false) { | ||||
|         fclose($this->fileHandle); | ||||
|       } | ||||
|       $this->fileHandle = false; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										29
									
								
								core/External/ZipStream/Writer.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										29
									
								
								core/External/ZipStream/Writer.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| <?php | ||||
| 
 | ||||
| /** | ||||
|  * Copyright (c) Borago 2019 | ||||
|  * | ||||
|  * This software is provided 'as-is', without any express or implied | ||||
|  * warranty. In no event will the authors be held liable for any damages | ||||
|  * arising from the use of this software. | ||||
|  * | ||||
|  * Permission is granted to anyone to use this software for any purpose, | ||||
|  * including commercial applications, and to alter it and redistribute it | ||||
|  * freely, subject to the following restrictions: | ||||
|  * | ||||
|  * 1. The origin of this software must not be misrepresented; you must not | ||||
|  *    claim that you wrote the original software. If you use this software | ||||
|  *    in a product, an acknowledgment in the product documentation would be | ||||
|  *    appreciated but is not required. | ||||
|  * 2. Altered source versions must be plainly marked as such, and must not be | ||||
|  *    misrepresented as being the original software. | ||||
|  * 3. This notice may not be removed or altered from any source distribution. | ||||
|  **/ | ||||
| 
 | ||||
| namespace External\ZipStream { | ||||
|   interface Writer { | ||||
|     public function write($data); | ||||
|     public function close(); | ||||
|     public function offset(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										72
									
								
								core/External/ZipStream/ZipStream.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										72
									
								
								core/External/ZipStream/ZipStream.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | ||||
| <?php | ||||
| 
 | ||||
| /** | ||||
|  * Copyright (c) Borago 2019 | ||||
|  * | ||||
|  * This software is provided 'as-is', without any express or implied | ||||
|  * warranty. In no event will the authors be held liable for any damages | ||||
|  * arising from the use of this software. | ||||
|  * | ||||
|  * Permission is granted to anyone to use this software for any purpose, | ||||
|  * including commercial applications, and to alter it and redistribute it | ||||
|  * freely, subject to the following restrictions: | ||||
|  * | ||||
|  * 1. The origin of this software must not be misrepresented; you must not | ||||
|  *    claim that you wrote the original software. If you use this software | ||||
|  *    in a product, an acknowledgment in the product documentation would be | ||||
|  *    appreciated but is not required. | ||||
|  * 2. Altered source versions must be plainly marked as such, and must not be | ||||
|  *    misrepresented as being the original software. | ||||
|  * 3. This notice may not be removed or altered from any source distribution. | ||||
|  **/ | ||||
| 
 | ||||
| namespace External\ZipStream { | ||||
|   class ZipStream { | ||||
|     private $writer = null; | ||||
|     private $files = []; | ||||
| 
 | ||||
|     public function __construct($writer) { | ||||
|       $this->writer = $writer; | ||||
|     } | ||||
| 
 | ||||
|     public function saveFile($file) { | ||||
|       $isSymlink = false; //currently not used
 | ||||
|       foreach ($this->files as $f) { | ||||
|         if ($f->name() == $file->name()) { | ||||
|           return false; | ||||
|         } | ||||
|         if ($f->sha256() == $file->sha256()) { | ||||
|           $isSymlink = true; | ||||
|         } | ||||
|       } | ||||
|       $file->setOffset($this->writer->offset()); | ||||
|       $this->writer->write($file->readLocalFileHeader()); | ||||
|       while (($buffer = $file->readFileData()) !== null) { | ||||
|         $this->writer->write($buffer); | ||||
|       } | ||||
|       $this->writer->write($file->readDataDescriptor()); | ||||
|       $this->files[] = $file; | ||||
|       $file->closeHandle(); | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     public function close() { | ||||
|       $size = 0; | ||||
|       $offset = $this->writer->offset(); | ||||
|       foreach ($this->files as $file) { | ||||
|         $size += $this->writer->write($file->readCentralDirectoryHeader()); | ||||
|       } | ||||
| 
 | ||||
|       $data = ""; | ||||
|       $data .= "\x50\x4b\x05\x06"; | ||||
|       $data .= "\x00\x00"; //number of disks
 | ||||
|       $data .= "\x00\x00"; //number of the disk with the start of the central directory
 | ||||
|       $data .= pack("v", count($this->files)); //total number of entries in the central directory on this disk
 | ||||
|       $data .= pack("v", count($this->files)); //total number of entries in the central directory
 | ||||
|       $data .= pack("V", $size); //size of the central directory
 | ||||
|       $data .= pack("V", $offset); //offset of start of central directory with respect to the starting disk number
 | ||||
|       $data .= "\x0\x0"; //comment length
 | ||||
|       $this->writer->write($data); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -254,20 +254,13 @@ class User extends ApiObject { | ||||
| 
 | ||||
|   public function processVisit() { | ||||
|     if ($this->sql && $this->sql->isConnected() && isset($_COOKIE["PHPSESSID"]) && !empty($_COOKIE["PHPSESSID"])) { | ||||
| 
 | ||||
|       if ($this->isBot()) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       $cookie = $_COOKIE["PHPSESSID"]; | ||||
|       $day = (new DateTime())->format("Ymd"); | ||||
| 
 | ||||
|       $this->sql->insert("Visitor", array("cookie", "day")) | ||||
|         ->addRow($cookie, $day) | ||||
|         ->onDuplicateKeyStrategy(new UpdateStrategy( | ||||
|           array("month", "cookie"), | ||||
|           array("count" => new Add("Visitor.count", 1)))) | ||||
|         ->execute(); | ||||
|       $req = new \Api\Visitors\ProcessVisit($this); | ||||
|       $req->execute(array("cookie" => $cookie)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <?php | ||||
| 
 | ||||
| define("WEBBASE_VERSION", "1.0.4"); | ||||
| define("WEBBASE_VERSION", "1.2.0"); | ||||
| 
 | ||||
| function getProtocol() { | ||||
|   return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https" : "http"; | ||||
| @ -75,6 +75,10 @@ function replaceCssSelector($sel) { | ||||
|   return preg_replace("~[.#<>]~", "_", preg_replace("~[:\-]~", "", $sel)); | ||||
| } | ||||
| 
 | ||||
| function urlId($str) { | ||||
|   return urlencode(htmlspecialchars(preg_replace("[: ]","-", $str))); | ||||
| } | ||||
| 
 | ||||
| function getClassPath($class, $suffix = true) { | ||||
|   $path = str_replace('\\', '/', $class); | ||||
|   $path = array_values(array_filter(explode("/", $path))); | ||||
| @ -134,7 +138,6 @@ function serveStatic(string $webRoot, string $file) { | ||||
|     $length = $size; | ||||
| 
 | ||||
|     if (isset($_SERVER['HTTP_RANGE'])) { | ||||
|       $partialContent = true; | ||||
|       preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches); | ||||
|       $offset = intval($matches[1]); | ||||
|       $length = intval($matches[2]) - $offset; | ||||
|  | ||||
							
								
								
									
										3
									
								
								fileControlPanel/.babelrc
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										3
									
								
								fileControlPanel/.babelrc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| { | ||||
|   "presets": ["@babel/preset-env", "@babel/preset-react"] | ||||
| } | ||||
							
								
								
									
										24
									
								
								fileControlPanel/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										24
									
								
								fileControlPanel/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||||
| 
 | ||||
| # dependencies | ||||
| /node_modules | ||||
| /.pnp | ||||
| .pnp.js | ||||
| 
 | ||||
| # testing | ||||
| /coverage | ||||
| 
 | ||||
| # production | ||||
| /build | ||||
| public/ | ||||
| 
 | ||||
| # misc | ||||
| .DS_Store | ||||
| .env.local | ||||
| .env.development.local | ||||
| .env.test.local | ||||
| .env.production.local | ||||
| 
 | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
							
								
								
									
										5
									
								
								fileControlPanel/.idea/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										5
									
								
								fileControlPanel/.idea/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| # Default ignored files | ||||
| /shelf/ | ||||
| /workspace.xml | ||||
| # Editor-based HTTP Client requests | ||||
| /httpRequests/ | ||||
							
								
								
									
										12
									
								
								fileControlPanel/.idea/fileControlPanel.iml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										12
									
								
								fileControlPanel/.idea/fileControlPanel.iml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <module type="WEB_MODULE" version="4"> | ||||
|   <component name="NewModuleRootManager"> | ||||
|     <content url="file://$MODULE_DIR$"> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/temp" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/.tmp" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/tmp" /> | ||||
|     </content> | ||||
|     <orderEntry type="inheritedJdk" /> | ||||
|     <orderEntry type="sourceFolder" forTests="false" /> | ||||
|   </component> | ||||
| </module> | ||||
| @ -0,0 +1,6 @@ | ||||
| <component name="InspectionProjectProfileManager"> | ||||
|   <profile version="1.0"> | ||||
|     <option name="myName" value="Project Default" /> | ||||
|     <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||
|   </profile> | ||||
| </component> | ||||
							
								
								
									
										6
									
								
								fileControlPanel/.idea/misc.xml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										6
									
								
								fileControlPanel/.idea/misc.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="WebPackConfiguration"> | ||||
|     <option name="mode" value="DISABLED" /> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										8
									
								
								fileControlPanel/.idea/modules.xml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										8
									
								
								fileControlPanel/.idea/modules.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="ProjectModuleManager"> | ||||
|     <modules> | ||||
|       <module fileurl="file://$PROJECT_DIR$/.idea/fileControlPanel.iml" filepath="$PROJECT_DIR$/.idea/fileControlPanel.iml" /> | ||||
|     </modules> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										6
									
								
								fileControlPanel/.idea/vcs.xml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										6
									
								
								fileControlPanel/.idea/vcs.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="VcsDirectoryMappings"> | ||||
|     <mapping directory="$PROJECT_DIR$/.." vcs="Git" /> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										31428
									
								
								fileControlPanel/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										31428
									
								
								fileControlPanel/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										46
									
								
								fileControlPanel/package.json
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										46
									
								
								fileControlPanel/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| { | ||||
|   "name": "file-control-panel", | ||||
|   "version": "0.1.0", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "axios": "^0.21.1", | ||||
|     "moment": "^2.26.0", | ||||
|     "react": "^16.13.1", | ||||
|     "react-collapse": "^5.0.1", | ||||
|     "react-dom": "^16.13.1", | ||||
|     "react-draft-wysiwyg": "^1.14.5", | ||||
|     "react-dropzone": "^11.2.4", | ||||
|     "react-router-dom": "^5.2.0", | ||||
|     "react-scripts": "^3.4.4", | ||||
|     "react-tooltip": "^4.2.13" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "build": "webpack --mode production && mv dist/main.js ../js/files.min.js", | ||||
|     "debug": "react-scripts start" | ||||
|   }, | ||||
|   "eslintConfig": { | ||||
|     "extends": "react-app" | ||||
|   }, | ||||
|   "browserslist": { | ||||
|     "production": [ | ||||
|       ">0.2%", | ||||
|       "not dead", | ||||
|       "not op_mini all" | ||||
|     ], | ||||
|     "development": [ | ||||
|       "last 1 chrome version", | ||||
|       "last 1 firefox version", | ||||
|       "last 1 safari version" | ||||
|     ] | ||||
|   }, | ||||
|   "proxy": "http://localhost", | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "^7.10.2", | ||||
|     "@babel/preset-env": "^7.10.2", | ||||
|     "@babel/preset-react": "^7.10.1", | ||||
|     "babel-loader": "^8.1.0", | ||||
|     "babel-polyfill": "^6.26.0", | ||||
|     "webpack": "^4.42.0", | ||||
|     "webpack-cli": "^4.3.1" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										96
									
								
								fileControlPanel/src/api.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										96
									
								
								fileControlPanel/src/api.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| import 'babel-polyfill'; | ||||
| import axios from "axios"; | ||||
| 
 | ||||
| export default class API { | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.loggedIn = false; | ||||
|         this.user = { }; | ||||
|     } | ||||
| 
 | ||||
|     csrfToken() { | ||||
|         return this.loggedIn ? this.user.session.csrf_token : null; | ||||
|     } | ||||
| 
 | ||||
|     async apiCall(method, params) { | ||||
|         params = params || { }; | ||||
|         const csrf_token = this.csrfToken(); | ||||
|         if (csrf_token) params.csrf_token = csrf_token; | ||||
|         let response = await axios.post("/api/" + method, params); | ||||
|         return response.data; | ||||
|     } | ||||
| 
 | ||||
|     async fetchUser() { | ||||
|         let response = await axios.get("/api/user/info"); | ||||
|         let data = response.data; | ||||
|         this.user = data["user"]; | ||||
|         this.loggedIn = data["loggedIn"]; | ||||
|         return data && data["success"] && data["loggedIn"]; | ||||
|     } | ||||
| 
 | ||||
|     async logout() { | ||||
|         return this.apiCall("user/logout"); | ||||
|     } | ||||
| 
 | ||||
|     validateToken(token) { | ||||
|         return this.apiCall("file/validateToken", { token: token }); | ||||
|     } | ||||
| 
 | ||||
|     listFiles() { | ||||
|         return this.apiCall("file/listFiles"); | ||||
|     } | ||||
| 
 | ||||
|     listTokens() { | ||||
|         return this.apiCall("file/listTokens"); | ||||
|     } | ||||
| 
 | ||||
|     delete(id, token=null) { | ||||
|         return this.apiCall("file/delete", { id: id, token: token }); | ||||
|     } | ||||
| 
 | ||||
|     revokeToken(token) { | ||||
|         return this.apiCall("file/revokeToken", { token: token }); | ||||
|     } | ||||
| 
 | ||||
|     createDownloadToken(durability, files) { | ||||
|         return this.apiCall("file/createDownloadToken", { files: files, durability: durability }); | ||||
|     } | ||||
| 
 | ||||
|     createUploadToken(durability, parentId=null, maxFiles=0, maxSize=0, extensions = "") { | ||||
|         return this.apiCall("file/createUploadToken", { parentId: parentId, durability: durability, maxFiles: maxFiles, maxSize: maxSize, extensions: extensions }); | ||||
|     } | ||||
| 
 | ||||
|     createDirectory(name, parentId = null) { | ||||
|         return this.apiCall("file/createDirectory", { name: name, parentId: parentId }); | ||||
|     } | ||||
| 
 | ||||
|     moveFiles(files, parentId = null) { | ||||
|         return this.apiCall("file/move", { id: files, parentId: parentId }); | ||||
|     } | ||||
| 
 | ||||
|     rename(id, name, token = null) { | ||||
|         return this.apiCall("file/rename", { id: id, name: name, token: token }); | ||||
|     } | ||||
| 
 | ||||
|     getRestrictions() { | ||||
|         return this.apiCall("file/getRestrictions"); | ||||
|     } | ||||
| 
 | ||||
|     async upload(file, token = null, parentId = null, cancelToken = null, onUploadProgress = null) { | ||||
|         const csrf_token = this.csrfToken(); | ||||
| 
 | ||||
|         const fd = new FormData(); | ||||
|         fd.append("file", file); | ||||
|         if (csrf_token) fd.append("csrf_token", csrf_token); | ||||
|         if (token) fd.append("token", token); | ||||
|         if (parentId) fd.append("parentId", parentId); | ||||
| 
 | ||||
|         let response = await axios.post('/api/file/upload', fd, { | ||||
|             headers: { 'Content-Type': 'multipart/form-data' }, | ||||
|             onUploadProgress: onUploadProgress || function () { }, | ||||
|             cancelToken : cancelToken.token | ||||
|         }); | ||||
| 
 | ||||
|         return response.data; | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										25
									
								
								fileControlPanel/src/elements/alert.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										25
									
								
								fileControlPanel/src/elements/alert.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| import Icon from "./icon"; | ||||
| import React from "react"; | ||||
| 
 | ||||
| export default function Alert(props) { | ||||
| 
 | ||||
|     const onClose = props.onClose || null; | ||||
|     const title = props.title || "Untitled Alert"; | ||||
|     const message = props.message || "Alert message"; | ||||
|     const type = props.type || "danger"; | ||||
| 
 | ||||
|     let icon = "ban"; | ||||
|     if (type === "warning") { | ||||
|         icon = "exclamation-triangle"; | ||||
|     } else if(type === "success") { | ||||
|         icon = "check"; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <div className={"alert alert-" + type + " alert-dismissible"}> | ||||
|             {onClose ? <button type="button" className={"close"} data-dismiss={"alert"} aria-hidden={"true"} onClick={onClose}>×</button> : null} | ||||
|             <h5><Icon icon={icon} className={"icon"} /> {title}</h5> | ||||
|             {message} | ||||
|         </div> | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										85
									
								
								fileControlPanel/src/elements/file-browser.css
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										85
									
								
								fileControlPanel/src/elements/file-browser.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| .file-row td { | ||||
|     padding: 0; | ||||
|     border: none; | ||||
|     vertical-align: middle; | ||||
|     font-size: 0.9em; | ||||
| } | ||||
| 
 | ||||
| .file-control-buttons { | ||||
|     display: grid; | ||||
|     grid-template-rows: auto auto; | ||||
|     grid-template-columns: auto auto; | ||||
| } | ||||
| 
 | ||||
| .file-control-buttons > button { | ||||
|     margin: 15px; | ||||
| } | ||||
| 
 | ||||
| .file-upload-container { | ||||
|     border: dotted; | ||||
|     margin: 18px; | ||||
|     padding: 15px; | ||||
|     min-height: 150px; | ||||
|     text-align: center; | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .file-upload-container > div > div { | ||||
|     display: grid; | ||||
|     grid-template-columns: auto auto auto auto; | ||||
| } | ||||
| 
 | ||||
| .uploaded-file { | ||||
|     max-width: 120px; | ||||
|     position: relative; | ||||
|     margin-bottom: 15px; | ||||
| } | ||||
| 
 | ||||
| .uploaded-file > span { | ||||
|     display: block; | ||||
|     word-wrap: break-word; | ||||
| } | ||||
| 
 | ||||
| .uploaded-file > .status-icon { | ||||
|     position: absolute; | ||||
|     top: -9px; | ||||
|     right: 25px; | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .uploaded-file > .cancel-button { | ||||
|     position: absolute; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     top: 15px; | ||||
|     bottom: 0; | ||||
|     opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .uploaded-file:hover > .file-icon { | ||||
|     opacity: 0.5; | ||||
| } | ||||
| 
 | ||||
| .uploaded-file:hover > .cancel-button { | ||||
|     opacity: 1.0; | ||||
| } | ||||
| 
 | ||||
| .clickable { cursor: pointer; } | ||||
| .token-revoked td { text-decoration: line-through; } | ||||
| 
 | ||||
| .token-table td:not(:first-child), .token-table th:not(:first-child) { | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| .token-table td:nth-child(4) > i { | ||||
|     padding-left: 10px; | ||||
| } | ||||
| 
 | ||||
| .file-table td:nth-child(n+3), .file-table th:nth-child(n+3) { | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| .file-browser-restrictions { | ||||
|     display: grid; | ||||
|     grid-template-columns: repeat(4, auto); | ||||
| } | ||||
							
								
								
									
										630
									
								
								fileControlPanel/src/elements/file-browser.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										630
									
								
								fileControlPanel/src/elements/file-browser.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,630 @@ | ||||
| import * as React from "react"; | ||||
| import "./file-browser.css"; | ||||
| import Dropzone from "react-dropzone"; | ||||
| import Icon from "./icon"; | ||||
| import Alert from "./alert"; | ||||
| import {Popup} from "./popup"; | ||||
| import {useEffect, useState} from "react"; | ||||
| import axios from "axios"; | ||||
| 
 | ||||
| export function FileBrowser(props) { | ||||
| 
 | ||||
|     let files = props.files || {}; | ||||
|     let api = props.api; | ||||
|     let tokenObj = props.token || {valid: false}; | ||||
|     let onSelectFile = props.onSelectFile || function () { }; | ||||
|     let onFetchFiles = props.onFetchFiles || function () { }; | ||||
|     let directories = props.directories || {}; | ||||
|     let restrictions = props.restrictions || {maxFiles: 0, maxSize: 0, extensions: ""}; | ||||
| 
 | ||||
|     let [popup, setPopup] = useState({ visible: false, name: "", directory: 0, target: null, type: "createDirectory" }); | ||||
|     let [alerts, setAlerts] = useState([]); | ||||
|     let [filesToUpload, setFilesToUpload] = useState([]); | ||||
| 
 | ||||
|     function canUpload() { | ||||
|         return api.loggedIn || (tokenObj.valid && tokenObj.type === "upload"); | ||||
|     } | ||||
| 
 | ||||
|     function svgMiddle(key, scale = 1.0) { | ||||
|         let width = 48 * scale; | ||||
|         let height = 64 * scale; | ||||
| 
 | ||||
|         return <svg key={key} width={width} height={height} xmlns="http://www.w3.org/2000/svg"> | ||||
|             <g> | ||||
|                 <line y2="0" x2={width / 2} y1={height} x1={width / 2} strokeWidth="1.5" stroke="#000" fill="none"/> | ||||
|                 <line y2={height / 2} x2={width} y1={height / 2} x1={width / 2} strokeWidth="1.5" stroke="#000" | ||||
|                       fill="none"/> | ||||
|             </g> | ||||
|         </svg>; | ||||
|     } | ||||
| 
 | ||||
|     function svgEnd(key, scale = 1.0) { | ||||
|         let width = 48 * scale; | ||||
|         let height = 64 * scale; | ||||
| 
 | ||||
|         return <svg key={key} width={width} height={height} xmlns="http://www.w3.org/2000/svg"> | ||||
|             <g> | ||||
|                 { /* vertical line */} | ||||
|                 <line y2="0" x2={width / 2} y1={height / 2} x1={width / 2} strokeWidth="1.5" stroke="#000" fill="none"/> | ||||
|                 { /* horizontal line */} | ||||
|                 <line y2={height / 2} x2={width} y1={height / 2} x1={width / 2} strokeWidth="1.5" stroke="#000" | ||||
|                       fill="none"/> | ||||
|             </g> | ||||
|         </svg>; | ||||
|     } | ||||
| 
 | ||||
|     function svgLeft(key, scale = 1.0) { | ||||
|         let width = 48 * scale; | ||||
|         let height = 64 * scale; | ||||
|         return <svg key={key} width={width} height={height} xmlns="http://www.w3.org/2000/svg"> | ||||
|             <g> | ||||
|                 { /* vertical line */} | ||||
|                 <line y2="0" x2={width / 2} y1={height} x1={width / 2} strokeWidth="1.5" stroke="#000" fill="none"/> | ||||
|             </g> | ||||
|         </svg>; | ||||
|     } | ||||
| 
 | ||||
|     function createFileIcon(mimeType, size = "2x") { | ||||
|         let icon = ""; | ||||
|         if (mimeType) { | ||||
|             mimeType = mimeType.toLowerCase().trim(); | ||||
|             let types = ["image", "text", "audio", "video"]; | ||||
|             let languages = ["php", "java", "python", "cpp"]; | ||||
|             let archives = ["zip", "tar", "archive"]; | ||||
|             let [mainType, subType] = mimeType.split("/"); | ||||
|             if (mainType === "text" && languages.find(a => subType.includes(a))) { | ||||
|                 icon = "code"; | ||||
|             } else if (mainType === "application" && archives.find(a => subType.includes(a))) { | ||||
|                 icon = "archive"; | ||||
|             } else if (mainType === "application" && subType === "pdf") { | ||||
|                 icon = "pdf"; | ||||
|             } else if (mainType === "application" && (subType.indexOf("powerpoint") > -1 || subType.indexOf("presentation") > -1)) { | ||||
|                 icon = "powerpoint"; | ||||
|             } else if (mainType === "application" && (subType.indexOf("word") > -1 || subType.indexOf("opendocument") > -1)) { | ||||
|                 icon = "word"; | ||||
|             } else if (mainType === "application" && (subType.indexOf("excel") > -1 || subType.indexOf("sheet") > -1)) { | ||||
|                 icon = "excel"; | ||||
|             } else if (mainType === "application" && subType.indexOf("directory") > -1) { | ||||
|                 icon = "folder"; | ||||
|             } else if (types.indexOf(mainType) > -1) { | ||||
|                 if (mainType === "text") { | ||||
|                     icon = "alt"; | ||||
|                 } else { | ||||
|                     icon = mainType; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (icon !== "folder") { | ||||
|             icon = "file" + (icon ? ("-" + icon) : icon); | ||||
|         } | ||||
| 
 | ||||
|         return <Icon icon={icon} type={"far"} className={"p-1 align-middle file-icon fa-" + size}/> | ||||
|     } | ||||
| 
 | ||||
|     function formatSize(size) { | ||||
|         const suffixes = ["B", "KiB", "MiB", "GiB", "TiB"]; | ||||
|         let i = 0; | ||||
|         for (; i < suffixes.length && size >= 1024; i++) { | ||||
|             size /= 1024.0; | ||||
|         } | ||||
| 
 | ||||
|         if (i === 0 || Math.round(size) === size) { | ||||
|             return size + " " + suffixes[i]; | ||||
|         } else { | ||||
|             return size.toFixed(1) + " " + suffixes[i]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         let newFiles = filesToUpload.slice(); | ||||
|         for (let fileIndex = 0; fileIndex < newFiles.length; fileIndex++) { | ||||
|             if (typeof newFiles[fileIndex].progress === 'undefined') { | ||||
|                 onUpload(fileIndex); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     }, [filesToUpload]); | ||||
| 
 | ||||
|     function onAddUploadFiles(acceptedFiles, rejectedFiles) { | ||||
| 
 | ||||
|         if (rejectedFiles && rejectedFiles.length > 0) { | ||||
|             const filenames = rejectedFiles.map(f => f.file.name).join(", "); | ||||
|             pushAlert({msg: "The following files could not be uploaded due to given restrictions: " + filenames }, "Cannot upload file"); | ||||
|         } | ||||
| 
 | ||||
|         if (acceptedFiles && acceptedFiles.length > 0) { | ||||
|             let files = filesToUpload.slice(); | ||||
|             files.push(...acceptedFiles); | ||||
|             setFilesToUpload(files); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function getSelectedIds(items = null, recursive = true) { | ||||
|         let ids = []; | ||||
|         items = items || files; | ||||
|         for (const fileItem of Object.values(items)) { | ||||
|             if (fileItem.selected) { | ||||
|                 ids.push(fileItem.uid); | ||||
|             } | ||||
|             if (recursive && fileItem.isDirectory) { | ||||
|                 ids.push(...getSelectedIds(fileItem.items)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return ids; | ||||
|     } | ||||
| 
 | ||||
|     // TODO: add more mime type names or use an directory here?
 | ||||
|     function getTypeName(type) { | ||||
|         if (type.toLowerCase() === "directory") { | ||||
|             return "Directory"; | ||||
|         } | ||||
| 
 | ||||
|         switch (type.toLowerCase()) { | ||||
|             case "image/jpeg": | ||||
|                 return "JPEG-Image"; | ||||
|             case "image/png": | ||||
|                 return "PNG-Image"; | ||||
|             case "application/pdf": | ||||
|                 return "PDF-Document"; | ||||
|             case "text/plain": | ||||
|                 return "Text-Document" | ||||
|             case "application/x-dosexec": | ||||
|                 return "Windows Executable"; | ||||
|             case "application/vnd.oasis.opendocument.text": | ||||
|                 return "OpenOffice-Document"; | ||||
|             default: | ||||
|                 return type; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     let selectedIds = getSelectedIds(); | ||||
|     let selectedCount = selectedIds.length; | ||||
|     let uploadZone = <></>; | ||||
|     let writePermissions = canUpload(); | ||||
|     let uploadedFiles = []; | ||||
|     let alertElements = []; | ||||
| 
 | ||||
|     function createFileList(elements, indentation = 0) { | ||||
|         let rows = []; | ||||
|         let rowIndex = 0; | ||||
| 
 | ||||
|         const scale = 0.45; | ||||
|         const iconSize = "lg"; | ||||
| 
 | ||||
|         const values = Object.values(elements); | ||||
|         for (const fileElement of values) { | ||||
|             let name = fileElement.name; | ||||
|             let uid = fileElement.uid; | ||||
|             let type = (fileElement.isDirectory ? "Directory" : fileElement.mimeType); | ||||
|             let size = (fileElement.isDirectory ? "" : formatSize(fileElement.size)); | ||||
|             let mimeType = (fileElement.isDirectory ? "application/x-directory" : fileElement.mimeType); | ||||
|             let token = (tokenObj && tokenObj.valid ? "&token=" + tokenObj.value : ""); | ||||
|             let svg = []; | ||||
|             if (indentation > 0) { | ||||
|                 for (let i = 0; i < indentation - 1; i++) { | ||||
|                     svg.push(svgLeft(rowIndex + "-" + i, scale)); | ||||
|                 } | ||||
| 
 | ||||
|                 if (rowIndex === values.length - 1) { | ||||
|                     svg.push(svgEnd(rowIndex + "-end", scale)); | ||||
|                 } else { | ||||
|                     svg.push(svgMiddle(rowIndex + "-middle", scale)); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             rows.push( | ||||
|                 <tr key={"file-" + uid} data-id={uid} className={"file-row"}> | ||||
|                     <td> | ||||
|                         {svg} | ||||
|                         {createFileIcon(mimeType, iconSize)} | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         {fileElement.isDirectory ? name : | ||||
|                             <a href={"/api/file/download?id=" + uid + token} download={true}>{name}</a> | ||||
|                         } | ||||
|                     </td> | ||||
|                     <td>{getTypeName(type)}</td> | ||||
|                     <td>{size}</td> | ||||
|                     <td> | ||||
|                         <input type={"checkbox"} checked={!!fileElement.selected} | ||||
|                                onChange={(e) => onSelectFile(e, uid)} | ||||
|                         /> | ||||
|                         { writePermissions ? | ||||
|                             <Icon icon={"pencil-alt"} title={"Rename"} className={"ml-2 clickable text-secondary"} | ||||
|                                 style={{marginTop: "-17px"}} onClick={() => onPopupOpen("rename", uid)} /> : | ||||
|                             <></> } | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             ); | ||||
| 
 | ||||
|             if (fileElement.isDirectory) { | ||||
|                 rows.push(...createFileList(fileElement.items, indentation + 1)); | ||||
|             } | ||||
|             rowIndex++; | ||||
|         } | ||||
|         return rows; | ||||
|     } | ||||
| 
 | ||||
|     for (let i = 0; i < alerts.length; i++) { | ||||
|         const alert = alerts[i]; | ||||
|         alertElements.push( | ||||
|             <Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)}/> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     let options = []; | ||||
|     for (const [uid, dir] of Object.entries(directories)) { | ||||
|         options.push( | ||||
|             <option key={"option-" + dir} value={uid}>{dir}</option> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     function getAllowedExtensions() { | ||||
|         let extensions = restrictions.extensions || ""; | ||||
|         return extensions.split(",") | ||||
|             .map(ext => ext.trim()) | ||||
|             .map(ext => !ext.startsWith(".") && ext.length > 0 ? "." + ext : ext) | ||||
|             .join(","); | ||||
|     } | ||||
| 
 | ||||
|     function getRestrictions() { | ||||
|         return { | ||||
|             accept: getAllowedExtensions(), | ||||
|             maxFiles: restrictions.maxFiles, | ||||
|             maxSize: restrictions.maxSize | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     function onCancelUpload(e, i) { | ||||
|         e.stopPropagation(); | ||||
|         e.preventDefault(); | ||||
|         const cancelToken = filesToUpload[i].cancelToken; | ||||
|         if (cancelToken && filesToUpload[i].progress < 1) { | ||||
|             cancelToken.cancel("Upload cancelled"); | ||||
|         } | ||||
| 
 | ||||
|         let files = filesToUpload.slice(); | ||||
|         files.splice(i, 1); | ||||
|         setFilesToUpload(files); | ||||
|     } | ||||
| 
 | ||||
|     let rows = createFileList(files); | ||||
|     if (writePermissions) { | ||||
| 
 | ||||
|         for (let i = 0; i < filesToUpload.length; i++) { | ||||
|             const file = filesToUpload[i]; | ||||
|             const progress = Math.round((file.progress ?? 0) * 100); | ||||
|             const done = progress >= 100; | ||||
|             uploadedFiles.push( | ||||
|                 <span className={"uploaded-file"} key={i}> | ||||
|                         {createFileIcon(file.type, "3x")} | ||||
|                     <span>{file.name}</span> | ||||
|                     {!done ? | ||||
|                         <div className={"progress border border-primary position-relative"}> | ||||
|                             <div className={"progress-bar progress-bar-striped progress-bar-animated"} role={"progressbar"} | ||||
|                                  aria-valuenow={progress} aria-valuemin={"0"} aria-valuemax={"100"} | ||||
|                                  style={{width: progress + "%"}} /> | ||||
|                            <span className="justify-content-center d-flex position-absolute w-100" style={{top: "7px"}}> | ||||
|                                { progress + "%" } | ||||
|                            </span> | ||||
|                         </div> : <></> | ||||
|                     } | ||||
|                     <Icon icon={done ? (file.success ? "check" : "times") : "spinner"} | ||||
|                           className={"status-icon " + (done ? (file.success ? "text-success" : "text-danger") : "text-secondary")} /> | ||||
|                     <Icon icon={"times"} className={"text-danger cancel-button fa-2x"} | ||||
|                           title={"Cancel Upload"} onClick={(e) => onCancelUpload(e, i)}/> | ||||
|                 </span> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         uploadZone = <> | ||||
|             <div className={"p-3"}> | ||||
|                 <label><b>Upload Directory:</b></label> | ||||
|                 <select value={popup.directory} className={"form-control"} | ||||
|                         onChange={(e) => onPopupChange(e, "directory")}> | ||||
|                     {options} | ||||
|                 </select> | ||||
|             </div> | ||||
|             <Dropzone onDrop={onAddUploadFiles} {...getRestrictions()} > | ||||
|                 {({getRootProps, getInputProps}) => ( | ||||
|                     <section className={"file-upload-container"}> | ||||
|                         <div {...getRootProps()}> | ||||
|                             <input {...getInputProps()} /> | ||||
|                             <p>Drag 'n' drop some files here, or click to select files</p> | ||||
|                             {uploadedFiles.length === 0 ? | ||||
|                                 <Icon className={"mx-auto fa-3x text-black-50"} icon={"upload"}/> : | ||||
|                                 <div>{uploadedFiles}</div> | ||||
|                             } | ||||
|                         </div> | ||||
|                     </section> | ||||
|                 )} | ||||
|             </Dropzone> | ||||
|         </>; | ||||
|     } | ||||
| 
 | ||||
|     let singleButton = { | ||||
|         gridColumnStart: 1, | ||||
|         gridColumnEnd: 3, | ||||
|         width: "40%", | ||||
|         margin: "0 auto" | ||||
|     }; | ||||
| 
 | ||||
|     function createPopup() { | ||||
|         let title = ""; | ||||
|         let inputs = []; | ||||
| 
 | ||||
|         if (popup.type === "createDirectory" || popup.type === "moveFiles") { | ||||
|             inputs.push( | ||||
|                 <div className={"form-group"} key={"select-directory"}> | ||||
|                     <label>Destination Directory:</label> | ||||
|                     <select value={popup.directory} className={"form-control"} | ||||
|                             onChange={(e) => onPopupChange(e, "directory")}> | ||||
|                         {options} | ||||
|                     </select> | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (popup.type === "createDirectory" || popup.type === "rename") { | ||||
|             inputs.push( | ||||
|                 <div className={"form-group"} key={"input-name"}> | ||||
|                     <label>{ popup.type === "createDirectory" ? "Create Directory" : "New Name" }</label> | ||||
|                     <input type={"text"} className={"form-control"} value={popup.name} maxLength={32} | ||||
|                            placeholder={"Enter name…"} | ||||
|                            onChange={(e) => onPopupChange(e, "name")}/> | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (popup.type === "createDirectory") { | ||||
|             title = "Create Directory"; | ||||
|         } else if (popup.type === "moveFiles") { | ||||
|             title = "Move Files"; | ||||
|         } else if (popup.type === "rename") { | ||||
|             title = "Rename File or Directory"; | ||||
|         } | ||||
| 
 | ||||
|         return <Popup title={title} visible={popup.visible} buttons={["Ok", "Cancel"]} onClose={onPopupClose} | ||||
|                       onClick={onPopupButton}> | ||||
|             { inputs } | ||||
|         </Popup> | ||||
|     } | ||||
| 
 | ||||
|     return <> | ||||
|         <h4> | ||||
|             <Icon icon={"sync"} className={"mx-3 clickable small"} onClick={fetchFiles}/> | ||||
|             File Browser | ||||
|         </h4> | ||||
|         <table className={"table data-table file-table"}> | ||||
|             <thead> | ||||
|             <tr> | ||||
|                 <th/> | ||||
|                 <th>Name</th> | ||||
|                 <th>Type</th> | ||||
|                 <th>Size</th> | ||||
|                 <th/> | ||||
|             </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|             {rows.length > 0 ? rows : | ||||
|                 <tr> | ||||
|                     <td colSpan={4} className={"text-center text-black-50"}> | ||||
|                         No files uploaded yet | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             } | ||||
|             </tbody> | ||||
|         </table> | ||||
|         <div className={"file-control-buttons"}> | ||||
|             <button type={"button"} className={"btn btn-success"} disabled={selectedCount === 0} style={!writePermissions ? singleButton : {}} | ||||
|                     onClick={() => onDownload(selectedIds)}> | ||||
|                 <Icon icon={"download"} className={"mr-1"}/> | ||||
|                 Download Selected Files ({selectedCount}) | ||||
|             </button> | ||||
|             { | ||||
|                 writePermissions ? | ||||
|                     <> | ||||
|                         <button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0} | ||||
|                                 onClick={() => deleteFiles(selectedIds)}> | ||||
|                             <Icon icon={"trash"} className={"mr-1"}/> | ||||
|                             Delete Selected Files ({selectedCount}) | ||||
|                         </button> | ||||
|                         {api.loggedIn ? | ||||
|                             <> | ||||
|                                 <button type={"button"} className={"btn btn-info"} | ||||
|                                     onClick={(e) => onPopupOpen("createDirectory")}> | ||||
|                                 <Icon icon={"plus"} className={"mr-1"}/> | ||||
|                                 Create Directory | ||||
|                                 </button> | ||||
|                                 <button type={"button"} className={"btn btn-primary"} disabled={selectedCount === 0} | ||||
|                                         onClick={(e) => onPopupOpen("moveFiles")}> | ||||
|                                     <Icon icon={"plus"} className={"mr-1"}/> | ||||
|                                     Move Selected Files ({selectedCount}) | ||||
|                                 </button> | ||||
|                             </>: | ||||
|                             <></> | ||||
|                         } | ||||
|                     </> | ||||
|                     : <></> | ||||
|             } | ||||
|         </div> | ||||
|         {uploadZone} | ||||
|         <div className={"file-browser-restrictions px-4 mb-4"}> | ||||
|             <b>Restrictions:</b> | ||||
|             <span>Max. Files: {restrictions.maxFiles}</span> | ||||
|             <span>Max. Filesize: {formatSize(restrictions.maxSize)}</span> | ||||
|             <span>{restrictions.extensions ? "Allowed extensions: " + restrictions.extensions : "All extensions allowed"}</span> | ||||
|         </div> | ||||
|         <div> | ||||
|             {alertElements} | ||||
|         </div> | ||||
|         { createPopup() } | ||||
|     </>; | ||||
| 
 | ||||
|     function onPopupOpen(type, target = null) { | ||||
|         setPopup({...popup, visible: true, type: type, target: target}); | ||||
|     } | ||||
| 
 | ||||
|     function onPopupClose() { | ||||
|         setPopup({...popup, visible: false}); | ||||
|     } | ||||
| 
 | ||||
|     function onPopupChange(e, key) { | ||||
|         setPopup({...popup, [key]: e.target.value}); | ||||
|     } | ||||
| 
 | ||||
|     function onPopupButton(btn) { | ||||
| 
 | ||||
|         if (btn === "Ok") { | ||||
|             let parentId = popup.directory === 0 ? null : popup.directory; | ||||
|             if (popup.type === "createDirectory") { | ||||
|                 api.createDirectory(popup.name, parentId).then((res) => { | ||||
|                     if (!res.success) { | ||||
|                         pushAlert(res, "Error creating directory"); | ||||
|                     } else { | ||||
|                         fetchFiles(); | ||||
|                     } | ||||
|                 }); | ||||
|             } else if (popup.type === "moveFiles") { | ||||
|                 api.moveFiles(selectedIds, parentId).then((res) => { | ||||
|                     if (!res.success) { | ||||
|                         pushAlert(res, "Error moving files"); | ||||
|                     } else { | ||||
|                         fetchFiles(); | ||||
|                     } | ||||
|                 }); | ||||
|             } else if (popup.type === "rename") { | ||||
|                 api.rename(popup.target, popup.name, tokenObj.valid ? tokenObj.value : null).then((res) => { | ||||
|                     if (!res.success) { | ||||
|                         pushAlert(res, "Error renaming file or directory"); | ||||
|                     } else { | ||||
|                         fetchFiles(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         onPopupClose(); | ||||
|     } | ||||
| 
 | ||||
|     function removeUploadedFiles() { | ||||
|         let newFiles = filesToUpload.filter(file => !file.progress || file.progress < 1.0); | ||||
|         if (newFiles.length !== filesToUpload.length) { | ||||
|             setFilesToUpload(newFiles); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function fetchFiles() { | ||||
|         let promise; | ||||
| 
 | ||||
|         if (tokenObj.valid) { | ||||
|             promise = api.validateToken(tokenObj.value); | ||||
|         } else if (api.loggedIn) { | ||||
|             promise = api.listFiles() | ||||
|         } else { | ||||
|             return; // should never happen
 | ||||
|         } | ||||
| 
 | ||||
|         promise.then((res) => { | ||||
|             if (res) { | ||||
|                 onFetchFiles(res.files); | ||||
|                 removeUploadedFiles(); | ||||
|             } else { | ||||
|                 pushAlert(res); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     function pushAlert(res, title) { | ||||
|         let newAlerts = alerts.slice(); | ||||
|         newAlerts.push({type: "danger", message: res.msg, title: title}); | ||||
|         setAlerts(newAlerts); | ||||
|     } | ||||
| 
 | ||||
|     function removeAlert(i) { | ||||
|         if (i >= 0 && i < alerts.length) { | ||||
|             let newAlerts = alerts.slice(); | ||||
|             newAlerts.splice(i, 1); | ||||
|             setAlerts(newAlerts); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function deleteFiles(selectedIds) { | ||||
|         if (selectedIds && selectedIds.length > 0) { | ||||
|             let token = (api.loggedIn ? null : tokenObj.value); | ||||
|             api.delete(selectedIds, token).then((res) => { | ||||
|                 if (res.success) { | ||||
|                     fetchFiles(); | ||||
|                 } else { | ||||
|                     pushAlert(res); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function onUploadProgress(event, fileIndex) { | ||||
|         if (fileIndex < filesToUpload.length) { | ||||
|             let files = filesToUpload.slice(); | ||||
|             files[fileIndex].progress = event.loaded >= event.total ? 1 : event.loaded / event.total; | ||||
|             setFilesToUpload(files); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function onUpload(fileIndex) { | ||||
|         let token = (api.loggedIn ? null : tokenObj.value); | ||||
|         let parentId = ((!api.loggedIn || popup.directory === 0) ? null : popup.directory); | ||||
|         const file = filesToUpload[fileIndex]; | ||||
|         const cancelToken = axios.CancelToken.source(); | ||||
| 
 | ||||
|         let newFiles = filesToUpload.slice(); | ||||
|         newFiles[fileIndex].cancelToken = cancelToken; | ||||
|         newFiles[fileIndex].progress = 0; | ||||
|         setFilesToUpload(newFiles); | ||||
| 
 | ||||
|         api.upload(file, token, parentId, cancelToken, (e) => onUploadProgress(e, fileIndex)).then((res) => { | ||||
| 
 | ||||
|             let newFiles = filesToUpload.slice(); | ||||
|             newFiles[fileIndex].success = res.success; | ||||
|             setFilesToUpload(newFiles); | ||||
| 
 | ||||
|             if (res.success) { | ||||
|                 fetchFiles(); | ||||
|             } else { | ||||
|                 pushAlert(res); | ||||
|             } | ||||
|         }).catch((reason) => { | ||||
|             if (reason && reason.message !== "Upload cancelled") { | ||||
|                 pushAlert({ msg: reason }, "Error uploading files"); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     function onDownload(selectedIds) { | ||||
|         if (selectedIds && selectedIds.length > 0) { | ||||
|             let token = (api.loggedIn ? "" : "&token=" + tokenObj.value); | ||||
|             let ids = selectedIds.map(id => "id[]=" + id).join("&"); | ||||
|             let downloadUrl = "/api/file/download?" + ids + token; | ||||
|             fetch(downloadUrl) | ||||
|                 .then(response => { | ||||
|                     let header = response.headers.get("Content-Disposition") || ""; | ||||
|                     let fileNameFields = header.split(";").filter(c => c.trim().toLowerCase().startsWith("filename=")); | ||||
|                     let fileName = null; | ||||
|                     if (fileNameFields.length > 0) { | ||||
|                         fileName = fileNameFields[0].trim().substr("filename=".length); | ||||
|                     } else { | ||||
|                         fileName = null; | ||||
|                     } | ||||
| 
 | ||||
|                     response.blob().then(blob => { | ||||
|                         let url = window.URL.createObjectURL(blob); | ||||
|                         let a = document.createElement('a'); | ||||
|                         a.href = url; | ||||
|                         if (fileName !== null) a.download = fileName; | ||||
|                         a.click(); | ||||
|                     }); | ||||
|                 }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										24
									
								
								fileControlPanel/src/elements/icon.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										24
									
								
								fileControlPanel/src/elements/icon.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| import * as React from "react"; | ||||
| 
 | ||||
| export default function Icon(props) { | ||||
| 
 | ||||
|     let classes = props.className || []; | ||||
|     classes = Array.isArray(classes) ? classes : classes.toString().split(" "); | ||||
|     let type = props.type || "fas"; | ||||
|     let icon = props.icon; | ||||
| 
 | ||||
|     classes.push(type); | ||||
|     classes.push("fa-" + icon); | ||||
| 
 | ||||
|     if (icon === "spinner" || icon === "circle-notch") { | ||||
|         classes.push("fa-spin"); | ||||
|     } | ||||
| 
 | ||||
|     let newProps = {...props, className: classes.join(" ") }; | ||||
|     delete newProps["type"]; | ||||
|     delete newProps["icon"]; | ||||
| 
 | ||||
|     return ( | ||||
|         <i {...newProps} /> | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										46
									
								
								fileControlPanel/src/elements/popup.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										46
									
								
								fileControlPanel/src/elements/popup.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| import React from 'react'; | ||||
| 
 | ||||
| export function Popup(props) { | ||||
| 
 | ||||
|     let buttonNames = props.buttons || ["Ok", "Cancel"]; | ||||
|     let onClick     = props.onClick || function () { }; | ||||
|     let visible     = !!props.visible; | ||||
|     let title       = props.title || "Popup Title"; | ||||
|     let onClose     = props.onClose || function() { }; | ||||
| 
 | ||||
|     let buttons = []; | ||||
|     const colors = ["primary", "secondary", "success", "warning", "danger"]; | ||||
|     for (let i = 0; i < buttonNames.length; i++) { | ||||
|         let name = buttonNames[i]; | ||||
|         let color = colors[i % colors.length]; | ||||
|         buttons.push( | ||||
|             <button key={"btn-" + i} type={"button"} className={"btn btn-" + color} data-dismiss={"modal"} | ||||
|                     onClick={() => onClick(name)}> | ||||
|                 {name} | ||||
|             </button> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return <> | ||||
|         <div className={"modal fade" + (visible ? " show" : "")} tabIndex="-1" role="dialog" style={{display: (visible) ? "block" : "none"}}> | ||||
|             <div className="modal-dialog" role="document"> | ||||
|                 <div className="modal-content"> | ||||
|                     <div className="modal-header"> | ||||
|                         <h5 className="modal-title">{title}</h5> | ||||
|                         <button type="button" className="close" aria-label="Close" onClick={onClose}> | ||||
|                             <span aria-hidden="true">×</span> | ||||
|                         </button> | ||||
|                     </div> | ||||
|                     <div className="modal-body"> | ||||
|                         {props.children} | ||||
|                     </div> | ||||
|                     <div className="modal-footer"> | ||||
|                         {buttons} | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         {visible ? <div className={"modal-backdrop fade show"}/> : <></>} | ||||
|     </>; | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										284
									
								
								fileControlPanel/src/elements/token-list.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										284
									
								
								fileControlPanel/src/elements/token-list.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,284 @@ | ||||
| import * as React from "react"; | ||||
| import Icon from "./icon"; | ||||
| import moment from "moment"; | ||||
| import {Popup} from "./popup"; | ||||
| import Alert from "./alert"; | ||||
| import {useEffect, useState} from "react"; | ||||
| import ReactTooltip from 'react-tooltip'; | ||||
| 
 | ||||
| export function TokenList(props) { | ||||
| 
 | ||||
|     let api = props.api; | ||||
|     let selectedFiles = props.selectedFiles || []; | ||||
|     let directories   = props.directories || {}; | ||||
| 
 | ||||
|     let [tokens, setTokens] = useState(null); | ||||
|     let [alerts, setAlerts] = useState([]); | ||||
|     let [hideRevoked, setHideRevoked] = useState(true); | ||||
|     let [popup, setPopup] = useState({ | ||||
|         tokenType: "download", | ||||
|         maxFiles: 0, | ||||
|         maxSize: 0, | ||||
|         extensions: "", | ||||
|         durability: 24 * 60 * 2, | ||||
|         visible: false, | ||||
|         directory: 0 | ||||
|     }); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setTimeout(() => { | ||||
|             if (tokens) { | ||||
|                 let hasChanged = false; | ||||
|                 let newTokens = tokens.slice(); | ||||
|                 for (let token of newTokens) { | ||||
|                     if (token.tooltip) hasChanged = true; | ||||
|                     token.tooltip = false; | ||||
|                 } | ||||
|                 if (hasChanged) setTokens(newTokens); | ||||
|             } | ||||
|         }, 1500); | ||||
|     }, [tokens]); | ||||
| 
 | ||||
|     function fetchTokens() { | ||||
|         api.listTokens().then((res) => { | ||||
|             if (res) { | ||||
|                 setTokens(res.tokens); | ||||
|             } else { | ||||
|                 pushAlert(res, "Error fetching tokens"); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     let rows = []; | ||||
|     if (tokens === null) { | ||||
|         fetchTokens(); | ||||
|     } else { | ||||
|         for (let i = 0; i < tokens.length; i++) { | ||||
|             const token = tokens[i]; | ||||
|             const validUntil = token.valid_until; | ||||
|             const revoked = validUntil !== null && moment(validUntil).isSameOrBefore(new Date()); | ||||
|             if (revoked && hideRevoked) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const timeStr = (validUntil === null ? "Forever" : moment(validUntil).format("Do MMM YYYY, HH:mm")); | ||||
|             const tooltipPropsRevoke = (revoked ? {} : { | ||||
|                 "data-tip": "Revoke", "data-place": "bottom", "data-type": "error", | ||||
|                 "data-effect": "solid", "data-for": "tooltip-token-" + token.uid, | ||||
|             }); | ||||
| 
 | ||||
|             const tooltipPropsCopy = (revoked ? {} : { | ||||
|                 "data-tip": "", "data-place": "bottom", "data-type": "info", | ||||
|                 "data-effect": "solid", "data-for": "tooltip-token-" + token.uid, | ||||
|             }); | ||||
| 
 | ||||
|             rows.push( | ||||
|                 <tr key={"token-" + token.uid} className={revoked ? "token-revoked" : ""}> | ||||
|                     <td>{token.token}</td> | ||||
|                     <td>{token.type}</td> | ||||
|                     <td>{timeStr}</td> | ||||
|                     <td> | ||||
|                         { revoked ? <></> : <ReactTooltip id={"tooltip-token-" + token.uid} | ||||
|                            getContent={(x) => { if (x === "Revoke") return x; return (!!token.tooltip ? "Coped successfully!" : "Copy to Clipboard"); }} /> } | ||||
|                         <Icon icon={"times"} className={"clickable text-" + (revoked ? "secondary" : "danger")} | ||||
|                               onClick={() => (revoked ? null : onRevokeToken(token.token))} disabled={revoked} | ||||
|                               {...tooltipPropsRevoke} /> | ||||
|                         <Icon icon={"save"} className={"clickable text-" + (revoked ? "secondary" : "info")} | ||||
|                               onClick={() => (revoked ? null : onCopyToken(i))} disabled={revoked} | ||||
|                               {...tooltipPropsCopy} /> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     let alertElements = []; | ||||
|     for (let i = 0; i < alerts.length; i++) { | ||||
|         const alert = alerts[i]; | ||||
|         alertElements.push( | ||||
|             <Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)}/> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     let options = []; | ||||
|     for (const [uid, dir] of Object.entries(directories)) { | ||||
|         options.push( | ||||
|             <option key={"option-" + dir} value={uid}>{dir}</option> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return <> | ||||
|         <h4> | ||||
|             <Icon icon={"sync"} className={"mx-3 clickable small"} onClick={fetchTokens}/> | ||||
|             Tokens | ||||
|         </h4> | ||||
|         <div className={"form-check p-3 ml-3"}> | ||||
|             <input type={"checkbox"} checked={hideRevoked} name={"hide-revoked"} | ||||
|                    className={"form-check-input"} style={{marginTop: "0.2rem"}} | ||||
|                    onChange={(e) => setHideRevoked(e.target.checked)}/> | ||||
|             <label htmlFor={"hide-revoked"} className={"form-check-label pl-2"}>Hide revoked</label> | ||||
|         </div> | ||||
|         <table className={"table token-table"}> | ||||
|             <thead> | ||||
|             <tr> | ||||
|                 <th>Token</th> | ||||
|                 <th>Type</th> | ||||
|                 <th>Valid Until</th> | ||||
|                 <th/> | ||||
|             </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|             {rows.length > 0 ? rows : | ||||
|                 <tr> | ||||
|                     <td colSpan={4} className={"text-center text-black-50"}> | ||||
|                         No active tokens connected with this account | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             } | ||||
|             </tbody> | ||||
|         </table> | ||||
|         <div> | ||||
|             <button type={"button"} className={"btn btn-success m-2"} onClick={onPopupOpen}> | ||||
|                 <Icon icon={"plus"} className={"mr-1"}/> | ||||
|                 Create Token | ||||
|             </button> | ||||
|         </div> | ||||
|         <div> | ||||
|             {alertElements} | ||||
|         </div> | ||||
|         <Popup title={"Create Token"} visible={popup.visible} buttons={["Ok", "Cancel"]} | ||||
|                onClose={onPopupClose} onClick={onPopupButton}> | ||||
|             <div className={"form-group"}> | ||||
|                 <label>Token Durability in minutes (0 = forever):</label> | ||||
|                 <input type={"number"} min={0} className={"form-control"} | ||||
|                        value={popup.durability} onChange={(e) => onPopupChange(e, "durability")}/> | ||||
|             </div> | ||||
|             <div className="form-group"> | ||||
|                 <label>Token Type:</label> | ||||
|                 <select value={popup.tokenType} className={"form-control"} | ||||
|                         onChange={(e) => onPopupChange(e, "tokenType")}> | ||||
|                     <option value={"upload"}>Upload</option> | ||||
|                     <option value={"download"}>Download</option> | ||||
|                 </select> | ||||
|             </div> | ||||
|             {popup.tokenType === "upload" ? | ||||
|                 <> | ||||
|                     <div className={"form-group"}> | ||||
|                         <label>Destination Directory:</label> | ||||
|                         <select value={popup.directory} className={"form-control"} | ||||
|                                 onChange={(e) => onPopupChange(e, "directory")}> | ||||
|                             { options } | ||||
|                         </select> | ||||
|                     </div> | ||||
|                     <b>Upload Restrictions:</b> | ||||
|                     <div className={"form-group"}> | ||||
|                         <label>Max. Files (0 = unlimited):</label> | ||||
|                         <input type={"number"} min={0} max={25} className={"form-control"} | ||||
|                                value={popup.maxFiles} | ||||
|                                onChange={(e) => onPopupChange(e, "maxFiles")}/> | ||||
|                     </div> | ||||
|                     <div className={"form-group"}> | ||||
|                         <label>Max. Size per file in MB (0 = unlimited):</label> | ||||
|                         <input type={"number"} min={0} max={10} className={"form-control"} | ||||
|                                value={popup.maxSize} onChange={(e) => onPopupChange(e, "maxSize")}/> | ||||
|                     </div> | ||||
|                     <div className={"form-group"}> | ||||
|                         <label>Allowed Extensions:</label> | ||||
|                         <input type={"text"} placeholder={"(no restrictions)"} maxLength={256} | ||||
|                                className={"form-control"} | ||||
|                                value={popup.extensions} | ||||
|                                onChange={(e) => onPopupChange(e, "extensions")}/> | ||||
|                     </div> | ||||
|                 </> : | ||||
|                 <></> | ||||
|             } | ||||
|         </Popup> | ||||
|     </>; | ||||
| 
 | ||||
|     function pushAlert(res, title) { | ||||
|         let newAlerts = alerts.slice(); | ||||
|         newAlerts.push({type: "danger", message: res.msg, title: title}); | ||||
|         setAlerts(newAlerts); | ||||
|     } | ||||
| 
 | ||||
|     function removeAlert(i) { | ||||
|         if (i >= 0 && i < alerts.length) { | ||||
|             let newAlerts = alerts.slice(); | ||||
|             newAlerts.splice(i, 1); | ||||
|             setAlerts(newAlerts); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function onRevokeToken(token) { | ||||
|         api.revokeToken(token).then((res) => { | ||||
|             if (res.success) { | ||||
|                 let newTokens = tokens.slice(); | ||||
|                 for (const tokenObj of newTokens) { | ||||
|                     if (tokenObj.token === token) { | ||||
|                         tokenObj.valid_until = moment(); | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 setTokens(newTokens); | ||||
|             } else { | ||||
|                 pushAlert(res, "Error revoking token"); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     function onPopupOpen() { | ||||
|         setPopup({...popup, visible: true}); | ||||
|     } | ||||
| 
 | ||||
|     function onPopupClose() { | ||||
|         setPopup({...popup, visible: false}); | ||||
|     } | ||||
| 
 | ||||
|     function onPopupChange(e, key) { | ||||
|         setPopup({...popup, [key]: e.target.value}); | ||||
|     } | ||||
| 
 | ||||
|     function onPopupButton(btn) { | ||||
| 
 | ||||
|         if (btn === "Ok") { | ||||
|             let durability = popup.durability; | ||||
|             let validUntil = (durability === 0 ? null : moment().add(durability, "hours").format("YYYY-MM-DD HH:mm:ss")); | ||||
|             if (popup.tokenType === "download") { | ||||
|                 api.createDownloadToken(durability, selectedFiles).then((res) => { | ||||
|                     if (!res.success) { | ||||
|                         pushAlert(res, "Error creating token"); | ||||
|                     } else { | ||||
|                         let newTokens = tokens.slice(); | ||||
|                         newTokens.push({token: res.token, valid_until: validUntil, type: "download"}); | ||||
|                         setTokens(newTokens); | ||||
|                     } | ||||
|                 }); | ||||
|             } else if (popup.tokenType === "upload") { | ||||
|                 let parentId = popup.directory === 0 ? null : popup.directory; | ||||
|                 api.createUploadToken(durability, parentId, popup.maxFiles, popup.maxSize, popup.extensions).then((res) => { | ||||
|                     if (!res.success) { | ||||
|                         pushAlert(res, "Error creating token"); | ||||
|                     } else { | ||||
|                         let newTokens = tokens.slice(); | ||||
|                         newTokens.push({uid: res.tokenId, token: res.token, valid_until: validUntil, type: "upload"}); | ||||
|                         setTokens(newTokens); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         onPopupClose(); | ||||
|     } | ||||
| 
 | ||||
|     function onCopyToken(index) { | ||||
|         let newTokens = tokens.slice(); | ||||
|         let token = newTokens[index].token; | ||||
|         let url = window.location.href; | ||||
|         if (!url.endsWith("/")) url += "/"; | ||||
|         url += token; | ||||
|         navigator.clipboard.writeText(url); | ||||
|         newTokens[index].tooltip = true; | ||||
|         setTokens(newTokens); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										230
									
								
								fileControlPanel/src/index.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										230
									
								
								fileControlPanel/src/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,230 @@ | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| import API from "./api"; | ||||
| import Icon from "./elements/icon"; | ||||
| import {FileBrowser} from "./elements/file-browser"; | ||||
| import {TokenList} from "./elements/token-list"; | ||||
| 
 | ||||
| class FileControlPanel extends React.Component { | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|         this.api = new API(); | ||||
|         this.state = { | ||||
|             loaded: false, | ||||
|             validatingToken: false, | ||||
|             errorMessage: "", | ||||
|             user: { }, | ||||
|             token: { valid: false, value: "", validUntil: null, type: null }, | ||||
|             files: {}, | ||||
|             restrictions: { maxFiles: 0, maxSize: 0, extensions: "" } | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     onFetchFiles(files) { | ||||
|         this.setState({ ...this.state, files: files }); | ||||
|     } | ||||
| 
 | ||||
|     getDirectories(prefix = "/", items = null) { | ||||
|         let directories = { } | ||||
|         items = items || this.state.files; | ||||
| 
 | ||||
|         if (prefix === "/") { | ||||
|             directories[0] = "/"; | ||||
|         } | ||||
| 
 | ||||
|         for(const fileItem of Object.values(items)) { | ||||
|             if (fileItem.isDirectory) { | ||||
|                 let path = prefix + (prefix.length > 1 ? "/" : "") + fileItem.name; | ||||
|                 directories[fileItem.uid] = path; | ||||
|                 directories = Object.assign(directories, {...this.getDirectories(path, fileItem.items)}); | ||||
|             } | ||||
|         } | ||||
|         return directories; | ||||
|     } | ||||
| 
 | ||||
|     getSelectedIds(items = null, recursive = true) { | ||||
|         let ids = []; | ||||
|         items = items || this.state.files; | ||||
|         for (const fileItem of Object.values(items)) { | ||||
|             if (fileItem.selected) { | ||||
|                 ids.push(fileItem.uid); | ||||
|             } | ||||
|             if (recursive && fileItem.isDirectory) { | ||||
|                 ids.push(...this.getSelectedIds(fileItem.items)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return ids; | ||||
|     } | ||||
| 
 | ||||
|     onSelectAll(selected, items) { | ||||
|         for (const fileElement of Object.values(items)) { | ||||
|             fileElement.selected = selected; | ||||
|             if (fileElement.isDirectory) { | ||||
|                 this.onSelectAll(selected, fileElement.items); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     onSelectFile(e, uid, items=null) { | ||||
| 
 | ||||
|         let found = false; | ||||
|         let updatedFiles = (items === null) ? {...this.state.files} : items; | ||||
|         if (updatedFiles.hasOwnProperty(uid)) { | ||||
|             let fileElement = updatedFiles[uid]; | ||||
|             found = true; | ||||
|             fileElement.selected = e.target.checked; | ||||
|             if (fileElement.isDirectory) { | ||||
|                 this.onSelectAll(fileElement.selected, fileElement.items); | ||||
|             } | ||||
|         } else { | ||||
|             for (const fileElement of Object.values(updatedFiles)) { | ||||
|                 if (fileElement.isDirectory) { | ||||
|                     if (this.onSelectFile(e, uid, fileElement.items)) { | ||||
|                         if (!e.target.checked) { | ||||
|                             fileElement.selected = false; | ||||
|                         }/* else if (this.getSelectedIds(fileElement.items, false).length === Object.values(fileElement.items).length) { | ||||
|                             fileElement.selected = true; | ||||
|                         }*/ | ||||
|                         found = true; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (items === null) { | ||||
|             this.setState({ | ||||
|                 ...this.state, | ||||
|                 files: updatedFiles | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return found; | ||||
|     } | ||||
| 
 | ||||
|     onValidateToken(token = null) { | ||||
|         if (token === null) { | ||||
|             this.setState({ ...this.state, validatingToken: true, errorMessage: "" }); | ||||
|             token = this.state.token.value; | ||||
|         } | ||||
| 
 | ||||
|         this.api.validateToken(token).then((res) => { | ||||
|             let newState = { ...this.state, loaded: true, validatingToken: false }; | ||||
|             if (res.success) { | ||||
|                 newState.token = { ...this.state.token, valid: true, validUntil: res.token.valid_until, type: res.token.type }; | ||||
|                 if (!newState.token.value) { | ||||
|                     newState.token.value = token; | ||||
|                 } | ||||
|                 newState.files = res.files; | ||||
|                 newState.restrictions = res.restrictions; | ||||
|             } else { | ||||
|                 newState.token.value = (newState.token.value ? "" : token); | ||||
|                 newState.errorMessage = res.msg; | ||||
|             } | ||||
| 
 | ||||
|             this.setState(newState); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     onUpdateToken(e) { | ||||
|         this.setState({ ...this.state, token: { ...this.state.token, value: e.target.value } }); | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         const self = this; | ||||
|         const errorMessageShown = !!this.state.errorMessage; | ||||
| 
 | ||||
|         // still loading
 | ||||
|         if (!this.state.loaded) { | ||||
| 
 | ||||
|             let checkUser = true; | ||||
|             let pathName = window.location.pathname; | ||||
|             if (pathName.startsWith("/files")) { | ||||
|                 pathName = pathName.substr("/files".length); | ||||
|             } | ||||
| 
 | ||||
|             if (pathName.length > 1) { | ||||
|                 let end = (pathName.endsWith("/") ? pathName.length - 2 : pathName.length - 1); | ||||
|                 let start = (pathName.startsWith("/files/") ? ("/files/").length : 1); | ||||
|                 let token = pathName.substr(start, end); | ||||
|                 if (token) { | ||||
|                     this.onValidateToken(token); | ||||
|                     checkUser = false; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (checkUser) { | ||||
|                 this.api.fetchUser().then((isLoggedIn) => { | ||||
|                     if (isLoggedIn) { | ||||
|                         this.api.listFiles().then((res) => { | ||||
|                             this.setState({ ...this.state, loaded: true, user: this.api.user, files: res.files }); | ||||
|                             this.api.getRestrictions().then((res) => { | ||||
|                                 this.setState({ ...this.state, restrictions: res.restrictions }); | ||||
|                             }) | ||||
|                         }); | ||||
|                     } else { | ||||
|                         this.setState({ ...this.state, loaded: true, user: this.api.user }); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return <>Loading… <Icon icon={"spinner"} /></>; | ||||
|         } | ||||
|         // access granted
 | ||||
|         else if (this.api.loggedIn || this.state.token.valid) { | ||||
|             let selectedIds = this.getSelectedIds(); | ||||
|             let directories = this.getDirectories(); | ||||
|             let tokenList = (this.api.loggedIn ? | ||||
|                 <div className={"row"}> | ||||
|                     <div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}> | ||||
|                         <TokenList api={this.api} selectedFiles={selectedIds} directories={directories} /> | ||||
|                     </div> | ||||
|                 </div> : <></> | ||||
|             ); | ||||
| 
 | ||||
|             return <> | ||||
|                     <div className={"container mt-4"}> | ||||
|                     <div className={"row"}> | ||||
|                         <div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}> | ||||
|                             <h2>File Control Panel</h2> | ||||
|                             <FileBrowser files={this.state.files} token={this.state.token} api={this.api} | ||||
|                                          restrictions={this.state.restrictions} directories={directories} | ||||
|                                          onSelectFile={this.onSelectFile.bind(this)} | ||||
|                                          onFetchFiles={this.onFetchFiles.bind(this)}/> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     { tokenList } | ||||
|                 </div> | ||||
|             </>; | ||||
|         } else { | ||||
|             return <div className={"container mt-4"}> | ||||
|                 <div className={"row"}> | ||||
|                     <div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}> | ||||
|                         <h2>File Control Panel</h2> | ||||
|                         <form onSubmit={(e) => e.preventDefault()}> | ||||
|                             <label htmlFor={"token"}>Enter a file token to download or upload files</label> | ||||
|                             <input type={"text"} className={"form-control"} name={"token"} placeholder={"Enter token…"} maxLength={36} | ||||
|                                    value={this.state.token.value} onChange={(e) => self.onUpdateToken(e)}/> | ||||
|                             <button className={"btn btn-success mt-2"} onClick={() => this.onValidateToken()} disabled={this.state.validatingToken}> | ||||
|                                 { this.state.validatingToken ? <>Validating… <Icon icon={"spinner"}/></> : "Submit" } | ||||
|                             </button> | ||||
|                         </form> | ||||
|                         <div className={"alert alert-danger mt-2"} hidden={!errorMessageShown}> | ||||
|                             { this.state.errorMessage } | ||||
|                         </div> | ||||
|                         <div className={"mt-3"}> | ||||
|                             Or either <a href={"/admin"}>login</a> to access the file control panel. | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div>; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ReactDOM.render( | ||||
|     <FileControlPanel />, | ||||
|     document.getElementById('root') | ||||
| ); | ||||
							
								
								
									
										17
									
								
								fileControlPanel/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										17
									
								
								fileControlPanel/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| module.exports = { | ||||
|     module: { | ||||
|         rules: [ | ||||
|             { | ||||
|                 test: /\.(js|jsx)$/, | ||||
|                 exclude: /node_modules/, | ||||
|                 use: { | ||||
|                     loader: "babel-loader" | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.css$/, | ||||
|                 use: ['style-loader', 'css-loader'] | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										2
									
								
								files/uploaded/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										2
									
								
								files/uploaded/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| * | ||||
| !.htaccess | ||||
							
								
								
									
										1
									
								
								files/uploaded/.htaccess
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										1
									
								
								files/uploaded/.htaccess
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| DENY FROM ALL | ||||
| @ -1,5 +1,9 @@ | ||||
| $(document).ready(function () { | ||||
| 
 | ||||
|     function isRecaptchaEnabled() { | ||||
|         return (typeof grecaptcha !== 'undefined'); | ||||
|     } | ||||
| 
 | ||||
|     function showAlert(type, msg) { | ||||
|         let alert = $("#alertMessage"); | ||||
|         alert.text(msg); | ||||
| @ -27,6 +31,8 @@ $(document).ready(function () { | ||||
|     } | ||||
| 
 | ||||
|     // Login
 | ||||
|     $("#username").keypress(function (e) { if(e.which === 13) $("#password").focus(); }); | ||||
|     $("#password").keypress(function (e) { if(e.which === 13) $("#btnLogin").click(); }); | ||||
|     $("#btnLogin").click(function() { | ||||
|         const username = $("#username").val(); | ||||
|         const password = $("#password").val(); | ||||
| @ -45,7 +51,7 @@ $(document).ready(function () { | ||||
|                 btn.prop("disabled", false); | ||||
|                 $("#password").val(""); | ||||
|                 createdDiv.hide(); | ||||
|                 showAlert(res.msg); | ||||
|                 showAlert("danger", res.msg); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| @ -59,7 +65,6 @@ $(document).ready(function () { | ||||
|         let email = $("#email").val().trim(); | ||||
|         let password = $("#password").val(); | ||||
|         let confirmPassword = $("#confirmPassword").val(); | ||||
|         let siteKey = $("#siteKey").val().trim(); | ||||
| 
 | ||||
|         if (username === '' || email === '' || password === '' || confirmPassword === '') { | ||||
|             showAlert("danger", "Please fill out every field."); | ||||
| @ -67,7 +72,8 @@ $(document).ready(function () { | ||||
|             showAlert("danger", "Your passwords did not match."); | ||||
|         } else { | ||||
|             let params = { username: username, email: email, password: password, confirmPassword: confirmPassword }; | ||||
|             if (typeof grecaptcha !== 'undefined') { | ||||
|             if (isRecaptchaEnabled()) { | ||||
|                 let siteKey = $("#siteKey").val().trim(); | ||||
|                 grecaptcha.ready(function() { | ||||
|                     grecaptcha.execute(siteKey, {action: 'register'}).then(function(captcha) { | ||||
|                         params["captcha"] = captcha; | ||||
| @ -122,10 +128,10 @@ $(document).ready(function () { | ||||
| 
 | ||||
|         let btn = $(this); | ||||
|         let email = $("#email").val(); | ||||
|         let siteKey = $("#siteKey").val().trim(); | ||||
| 
 | ||||
|         let params = { email: email }; | ||||
|         if (typeof grecaptcha !== 'undefined') { | ||||
|         if (isRecaptchaEnabled()) { | ||||
|             let siteKey = $("#siteKey").val().trim(); | ||||
|             grecaptcha.ready(function() { | ||||
|                 grecaptcha.execute(siteKey, {action: 'resetPassword'}).then(function(captcha) { | ||||
|                     params["captcha"] = captcha; | ||||
|  | ||||
							
								
								
									
										26
									
								
								js/admin.min.js
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										26
									
								
								js/admin.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										302
									
								
								js/files.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										302
									
								
								js/files.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -4,8 +4,8 @@ const SUCCESFULL = 2; | ||||
| const ERROR = 3; | ||||
| 
 | ||||
| function setState(state) { | ||||
|   var li = $("#currentStep"); | ||||
|   var icon, color, text; | ||||
|   let li = $("#currentStep"); | ||||
|   let icon, color, text; | ||||
| 
 | ||||
|   switch (state) { | ||||
|     case PENDING: | ||||
| @ -44,23 +44,25 @@ function getCurrentStep() { | ||||
| 
 | ||||
| function sendRequest(params, done) { | ||||
|   setState(PENDING); | ||||
|   var success = false; | ||||
|   $("#status").hide(); | ||||
|   let success = false; | ||||
|   let statusBox = $("#status"); | ||||
| 
 | ||||
|   statusBox.hide(); | ||||
|   $.post("/index.php", params, function(data) { | ||||
|     if(data.success || data.step != getCurrentStep()) { | ||||
|     if(data.success || data.step !== getCurrentStep()) { | ||||
|       success = true; | ||||
|       window.location.reload(); | ||||
|     } else { | ||||
|       setState(ERROR); | ||||
|       $("#status").addClass("alert-danger"); | ||||
|       $("#status").html("An error occurred during intallation: " + data.msg); | ||||
|       $("#status").show(); | ||||
|       statusBox.addClass("alert-danger"); | ||||
|       statusBox.html("An error occurred during intallation: " + data.msg); | ||||
|       statusBox.show(); | ||||
|     } | ||||
|   }, "json").fail(function() { | ||||
|     setState(ERROR); | ||||
|     $("#status").addClass("alert-danger"); | ||||
|     $("#status").html("An error occurred during intallation. Try <a href=\"/index.php\">restarting the process</a>."); | ||||
|     $("#status").show(); | ||||
|     statusBox.addClass("alert-danger"); | ||||
|     statusBox.html("An error occurred during intallation. Try <a href=\"/index.php\">restarting the process</a>."); | ||||
|     statusBox.show(); | ||||
|   }).always(function() { | ||||
|     if(done) done(success); | ||||
|   }); | ||||
| @ -79,12 +81,13 @@ $(document).ready(function() { | ||||
| 
 | ||||
|   $("#btnSubmit").click(function() { | ||||
|     params = { }; | ||||
|     var textBefore = $("#btnSubmit").text(); | ||||
|     $("#btnSubmit").prop("disabled", true); | ||||
|     $("#btnSubmit").html("Submitting… <i class=\"fas fa-spinner fa-spin\">"); | ||||
|     let submitButton = $("#btnSubmit"); | ||||
|     let textBefore = submitButton.text(); | ||||
|     submitButton.prop("disabled", true); | ||||
|     submitButton.html("Submitting… <i class=\"fas fa-spinner fa-spin\">"); | ||||
|     $("#installForm .form-control").each(function() { | ||||
|       var type = $(this).attr("type") ?? $(this).prop("tagName").toLowerCase(); | ||||
|       var name = $(this).attr("name"); | ||||
|       let type = $(this).attr("type") ?? $(this).prop("tagName").toLowerCase(); | ||||
|       let name = $(this).attr("name"); | ||||
|       if(type === "text") { | ||||
|         params[name] = $(this).val().trim(); | ||||
|       } else if(type === "password" || type === "number") { | ||||
| @ -95,8 +98,8 @@ $(document).ready(function() { | ||||
|     }).promise().done(function() { | ||||
|       sendRequest(params, function(success) { | ||||
|         if(!success) { | ||||
|           $("#btnSubmit").prop("disabled",false); | ||||
|           $("#btnSubmit").text(textBefore); | ||||
|           submitButton.prop("disabled",false); | ||||
|           submitButton.text(textBefore); | ||||
|         } else { | ||||
|           setState(SUCCESFULL); | ||||
|         } | ||||
| @ -135,17 +138,16 @@ $(document).ready(function() { | ||||
|   }); | ||||
| 
 | ||||
|   // DATABASE PORT
 | ||||
|   var prevPort = $("#port").val(); | ||||
|   var prevDbms = $("#type option:selected").val(); | ||||
|   let prevPort = $("#port").val(); | ||||
|   let prevDbms = $("#type option:selected").val(); | ||||
|   function updateDefaultPort() { | ||||
|     var defaultPorts = { | ||||
|     let defaultPorts = { | ||||
|       "mysql": 3306, | ||||
|       "postgres": 5432, | ||||
|       "oracle": 1521 | ||||
|       "postgres": 5432 | ||||
|     }; | ||||
| 
 | ||||
|     var curDbms = $("#type option:selected").val(); | ||||
|     if(defaultPorts[prevDbms] == prevPort) { | ||||
|     let curDbms = $("#type option:selected").val(); | ||||
|     if(defaultPorts[prevDbms] === prevPort) { | ||||
|       $("#port").val(defaultPorts[curDbms]); | ||||
|     } | ||||
|   } | ||||
|  | ||||
							
								
								
									
										15045
									
								
								src/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										15045
									
								
								src/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user