checkForUpdates = $checkForUpdates; // Set Project Root directory and paths $this->root = dirname(__DIR__, 2); $this->paths = [ 'Root Folder' => $this->root . DIRECTORY_SEPARATOR, 'Cache Folder' => $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR, 'Tab Folder' => $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR, 'API Folder' => dirname(__DIR__, 1) . DIRECTORY_SEPARATOR ]; // Temp Set Errors $this->errors = E_ERROR;//E_ALL & ~E_NOTICE // Set current time $this->currentTime = gmdate('Y-m-d\TH:i:s\Z'); // Set variable if install is for official docker $this->docker = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Docker.txt')); // Set variable if install is for develop and set php Error levels $this->dev = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Dev.txt')); // Set variable if install is for demo $this->demo = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Demo.txt')); // Set variable if install has commit hash and variable to be used as hash for files $this->commit = ($this->docker && !$this->dev) ? file_get_contents(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Github.txt') : null; $this->fileHash = ($this->commit) ?? $this->version; $this->fileHash = trim($this->fileHash); // Set location path to user config path $this->chooseConfigFile(); // Set location path to default config path $this->defaultConfigPath = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'default.php'; // Load Config file $this->config = $this->config(); // Set cookie name for Organizr Instance $this->cookieName = ($this->hasConfig()) ? $this->config['uuid'] !== '' ? 'organizr_token_' . $this->config['uuid'] : 'organizr_token_temp' : 'organizr_token_temp'; // Set custom Error handler set_error_handler([$this, 'setAPIErrorResponse'], $this->errors); // Next Check PHP Version $this->checkPHP(); // Check Disk Space $this->checkDiskSpace(); // Set UUID for device $this->setDeviceUUID(); // Add Plugin prefix to plugin global $this->setPluginListNameFromConfigPrefix(); // Add database path to paths $this->addDatabaseToPaths(); // Set Start Execution Time $this->timeExecution = $this->timeExecution(); $this->phpErrors(); // Set organizr Logs and logger $this->logFile = $this->setOrganizrLog(); $this->setLoggerChannel(); // Connect to DB $this->connectDB(); // Check DB Writable $this->checkWritableDB(); // Get token form cookie and validate $this->setCurrentUser(); // might just run this at index $this->upgradeCheck(); // Is Page load Organizr OAuth? $this->checkForOrganizrOAuth(); // Is user Blacklisted? $this->checkIfUserIsBlacklisted(); } public function __destruct() { $this->disconnectDB(); } public function addDatabaseToPaths() { if ($this->hasConfig()) { $this->paths = array_merge($this->paths, ['DB Folder' => $this->config['dbLocation']]); } } public function chooseConfigFile() { $oldUserConfigPath = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php'; $userConfigPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php'; if (file_exists($userConfigPath) && file_exists($oldUserConfigPath)) { $this->userConfigPath = $userConfigPath; } elseif (file_exists($oldUserConfigPath)) { $this->makeDir(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR); if ($this->rcopy($oldUserConfigPath, $userConfigPath)) { $this->userConfigPath = $userConfigPath; @unlink($oldUserConfigPath); } else { $this->userConfigPath = $oldUserConfigPath; } } else { $this->userConfigPath = $userConfigPath; } } public function hasConfig() { return (file_exists($this->userConfigPath)) ?? false; } public function hasDatabase($file = null) { $databaseType = $this->config['driver']; if (!$this->hasConfig()) { return false; } switch (strtolower($databaseType)) { case 'sqlite3': $file = $file ? $this->config['dbLocation'] . $file : $this->config['dbLocation'] . $this->config['dbName']; return [ 'driver' => 'sqlite3', 'database' => $file ]; case 'mysql': case 'mysqli': $db = $file ? 'tempMigration' : $this->config['dbName']; return [ 'driver' => 'mysqli', 'host' => $this->config['dbHost'], 'username' => $this->config['dbUsername'], 'password' => $this->decrypt($this->config['dbPassword']), 'database' => $db, 'options' => [ MYSQLI_OPT_CONNECT_TIMEOUT => 60, ], 'flags' => MYSQLI_CLIENT_COMPRESS, ]; case 'postgre': $config = [ 'driver' => 'postgre', 'username' => $this->config['dbUsername'], 'password' => $this->decrypt($this->config['dbPassword']), 'persistent' => true, ]; $host = $this->qualifyURL($this->config['dbHost'], true); if ($host['port']) { $config = array_merge($config, ['port' => ltrim($host['port'], ':')]); } if ($host['host']) { $config = array_merge($config, ['host' => $host['host']]); } if (!$host['host'] && $host['path']) { $config = array_merge($config, ['host' => $host['path']]); } return $config; default: return false; } } protected function connectDB() { $databaseConnection = $this->hasDatabase(); //$this->prettyPrint($databaseConnection); if ($databaseConnection) { try { $this->db = new Connection($databaseConnection); } catch (Dibi\Exception $e) { $this->db = null; } } else { $this->db = null; } } public function disconnectDB() { if ($this->db) { $this->db->disconnect(); $this->db = null; unset($this->db); } } public function connectOtherDB($file = null) { $databaseConnection = $this->hasDatabase('tempMigration.db'); if ($databaseConnection) { try { $this->otherDb = new Connection($databaseConnection); } catch (Dibi\Exception $e) { $this->prettyPrint($e->getMessage()); $this->otherDb = null; } } else { $this->otherDb = null; } } public function setDeviceUUID() { if (!isset($_COOKIE['organizr_user_uuid'])) { $this->coookie('set', 'organizr_user_uuid', $this->gen_uuid(), 7); } } public function refreshDeviceUUID() { if (isset($_COOKIE['organizr_user_uuid'])) { $this->coookie('delete', 'organizr_user_uuid'); } $this->coookie('set', 'organizr_user_uuid', $this->gen_uuid(), 7); } public function setCurrentUser($validate = true) { $user = false; if ($this->hasDatabase()) { if ($this->hasCookie()) { $user = $this->getUserFromToken($_COOKIE[$this->cookieName]); } } $this->user = ($user) ?: $this->guestUser(); $this->setLoggerChannel(null, $this->user['username']); if ($validate) { $this->checkUserTokenForValidation(); } } public function checkUserTokenForValidation() { if ($this->hasDB()) { if ($this->hasCookie()) { $this->validateToken($_COOKIE[$this->cookieName]); } } } public function phpErrors() { $errorTypes = $this->dev ? E_ERROR | E_WARNING | E_PARSE | E_NOTICE : 0; // Temp overwrite for now $displayErrors = $this->dev ? 1 : 0; error_reporting($this->errors); ini_set('display_errors', $displayErrors); } public function checkForOrganizrOAuth() { // Oauth? if ($this->hasDB() && $this->user) { if ($this->user['groupID'] == '999') { $this->setLoggerChannel('OAuth')->debug('Starting OAuth login check'); $data = [ 'enabled' => $this->config['authProxyEnabled'], 'header_name' => $this->config['authProxyHeaderName'], 'header_name_email' => $this->config['authProxyHeaderNameEmail'], 'whitelist' => $this->config['authProxyWhitelist'], ]; if ($this->config['authProxyEnabled'] && ($this->config['authProxyHeaderName'] !== '' || $this->config['authProxyHeaderNameEmail'] !== '') && $this->config['authProxyWhitelist'] !== '') { if (isset($this->getallheadersi()[strtolower($this->config['authProxyHeaderName'])]) || isset($this->getallheadersi()[strtolower($this->config['authProxyHeaderNameEmail'])])) { $this->coookieSeconds('set', 'organizrOAuth', 'true', 20000, false); $this->setLoggerChannel('OAuth')->info('OAuth pre-check passed - adding organizrOAuth cookie', $data); } else { $data = array_merge($data, ['headers' => $this->getallheadersi()]); $this->setLoggerChannel('OAuth')->debug('Headers not set', $data); } } else { $this->setLoggerChannel('OAuth')->debug('OAuth not triggered', $data); } } } } public function checkIfUserIsBlacklisted() { if ($this->hasConfig()) { $currentIP = $this->userIP(); if ($this->config['blacklisted'] !== '') { if (in_array($currentIP, $this->arrayIP($this->config['blacklisted']))) { $this->setLoggerChannel('Authentication'); $this->logger->debug('User was sent to black hole', ['blacklist' => $this->config['blacklisted']]); die($this->showHTML('Blacklisted', $this->config['blacklistedMessage'])); } } } } public function checkDiskSpace($directory = './') { $readable = @is_readable($directory); if ($readable) { $disk = $this->checkDisk($directory); $diskLevels = [ 'warn' => 1000000000, 'warn_human_readable' => $this->human_filesize(1000000000, 0), 'error' => 100000000, 'error_human_readable' => $this->human_filesize(100000000, 0), ]; if ($disk['free']['raw'] <= $diskLevels['error']) { die($this->showHTML('Low Disk Space', 'You are dangerously low on disk space.
There is only ' . $disk['free']['human_readable'] . ' remaining.
Percent Used = ' . $disk['used']['percent_used'] . '%')); } elseif ($disk['free']['raw'] <= $diskLevels['warn']) { $this->warnings[] = 'You are low on disk space. There is only ' . $disk['free']['human_readable'] . ' remaining. This warning shows up because you are past the warning threshold of ' . $diskLevels['warn_human_readable']; } } return true; } public function getFreeSpace($directory = './') { $disk = disk_free_space($directory); return [ 'raw' => $disk, 'human_readable' => $this->human_filesize($disk, 0) ]; } public function getDiskSpace($directory = './') { $disk = disk_total_space($directory); return [ 'raw' => $disk, 'human_readable' => $this->human_filesize($disk, 0) ]; } public function getUsedSpace($directory = './') { $diskFree = $this->getFreeSpace($directory); $diskTotal = $this->getDiskSpace($directory); $diskUsed = $diskTotal['raw'] - $diskFree['raw']; $percentUsed = ($diskUsed / $diskTotal['raw']) * 100; $percentFree = 100 - $percentUsed; return [ 'raw' => $diskUsed, 'human_readable' => $this->human_filesize($diskUsed, 0), 'percent_used' => round($percentUsed), 'percent_free' => round($percentFree) ]; } public function checkDisk($directory = './') { $readable = @is_readable($directory); if ($readable) { return [ 'free' => $this->getFreeSpace($directory), 'used' => $this->getUsedSpace($directory), 'total' => $this->getDiskSpace($directory), ]; } else { return [ 'free' => 'error accessing path', 'used' => 'error accessing path', 'total' => 'error accessing path', ]; } } public function errorCodes($error = 000) { $errorCodes = [ 400 => [ 'type' => 'Bad Request', 'description' => 'The request was incorrect' ], 401 => [ 'type' => 'Unauthorized ', 'description' => 'You are not authorized to view this page' ], 402 => [ 'type' => 'Payment Required', 'description' => 'Payment required before you can view this page' ], 403 => [ 'type' => 'Forbidden', 'description' => 'You are forbidden to view this page' ], 404 => [ 'type' => 'Not Found', 'description' => 'The requested resource was not found' ], 405 => [ 'type' => 'Method Not Allowed', 'description' => 'The requested method is not allowed' ], 406 => [ 'type' => 'Not Acceptable', 'description' => 'There was an issue with the requests Headers' ], 407 => [ 'type' => 'Proxy Authentication Required', 'description' => 'Authentication is required and was not passed' ], 408 => [ 'type' => 'Request Time-out', 'description' => 'The request has timed out' ], 409 => [ 'type' => 'Conflict', 'description' => 'An error has occurred' ], 410 => [ 'type' => 'Gone', 'description' => 'The requested resource is no longer available and has been permanently removed' ], 411 => [ 'type' => 'Length Required', 'description' => 'The request can not be processed without a "Content-Length" header field' ], 412 => [ 'type' => 'Precondition Failed', 'description' => ' A header needed was not found' ], 413 => [ 'type' => 'Request Entity Too Large', 'description' => 'The query was too large to be processed by the server' ], 414 => [ 'type' => 'Request-URI Too Long', 'description' => 'The URI of the request was too long' ], 415 => [ 'type' => 'Unsupported Media Type', 'description' => 'The contents of the request has been submitted with invalid or out of defined media type' ], 416 => [ 'type' => 'Requested range not satisfiable', 'description' => 'The requested resource was part of an invalid or is not on the server' ], 417 => [ 'type' => 'Expectation Failed', 'description' => 'Expected Header was not found' ], 444 => [ 'type' => 'No Response', 'description' => 'Nothing was returned from server' ], 500 => [ 'type' => 'Internal Server Error', 'description' => 'An unexpected server error' ], 501 => [ 'type' => 'Not Implemented', 'description' => 'The functionality to process the request is not available from this server' ], 502 => [ 'type' => 'Bad Gateway', 'description' => 'The server could not fulfill its function as a gateway or proxy' ], 503 => [ 'type' => 'Service Unavailable', 'description' => 'The server is temporarily unavailable, due to overloading or maintenance' ], 504 => [ 'type' => 'Gateway Time-out', 'description' => 'The server could not fulfill its function as a gateway or proxy' ], 505 => [ 'type' => 'HTTP version not supported', 'description' => 'The used version of HTTP is not supported by the server or rejected' ], 507 => [ 'type' => 'Insufficient Storage', 'description' => 'The request could not be processed because the server disk space it currently is not sufficient' ], 509 => [ 'type' => 'Bandwidth Limit Exceeded', 'description' => 'The request was rejected, because otherwise the bandwidth would be exceeded' ], 510 => [ 'type' => 'Not Extended', 'description' => 'The request does not contain all information that is waiting for the requested server extension imperative' ], 000 => [ 'type' => 'Unexpected Error', 'description' => 'An unexpected error occurred' ], ]; return (isset($errorCodes[$error])) ? $errorCodes[$error] : $errorCodes[000]; } public function showTopBarHamburger() { if ($this->config['allowCollapsableSideMenu']) { if ($this->config['sideMenuCollapsed']) { return ''; } else { return ''; } } return ''; } public function showSideBarHamburger() { if ($this->config['allowCollapsableSideMenu']) { if (!$this->config['sideMenuCollapsed']) { return ''; } } return ''; } public function showSideBarText() { if ($this->config['allowCollapsableSideMenu']) { if (!$this->config['sideMenuCollapsed']) { return ''; } } return ''; } public function auth() { if ($this->hasDB()) { $this->setLoggerChannel('Auth'); if (isset($_GET['type'])) { switch (strtolower($_GET['type'])) { case 'whitelist': case 'white': case 'w': case 'wl': case 'allow': $_GET['whitelist'] = $_GET['ips'] ?? false; break; case 'blacklist': case 'black': case 'b': case 'bl': case 'deny': $_GET['blacklist'] = $_GET['ips'] ?? false; break; default: $this->setAPIResponse('error', $_GET['type'] . ' is not a valid type', 401); return true; } } $whitelist = $_GET['whitelist'] ?? false; $blacklist = $_GET['blacklist'] ?? false; $group = 0; $groupParam = ($_GET['group']) ?? 0; $redirect = false; if (isset($groupParam)) { if (is_numeric($groupParam)) { $group = (int)$groupParam; } else { $group = $this->getTabGroupByTabName($groupParam); } } $currentIP = $this->userIP(); $unlocked = !($this->user['locked'] == '1'); if (isset($this->user)) { $currentUser = $this->user['username']; $currentGroup = $this->user['groupID']; $currentEmail = $this->user['email']; } else { $currentUser = 'Guest'; $currentGroup = $this->getUserLevel(); $currentEmail = 'guest@guest.com'; } $userInfo = [ "user" => $currentUser, "group" => $currentGroup, "email" => $currentEmail, "user_ip" => $currentIP, "requested_group" => $group, "uuid" => $_COOKIE['organizr_user_uuid'] ?? 'n/a' ]; $this->logger->debug('Starting check', $userInfo); $responseMessage = 'User is not Authorized or User is locked'; if ($whitelist) { if (in_array($currentIP, $this->arrayIP($whitelist))) { $responseMessage = 'User is whitelisted'; $this->setAPIResponse('success', $responseMessage, 200, $userInfo); $this->logger->debug($responseMessage, $userInfo); return true; } } if ($blacklist) { if (in_array($currentIP, $this->arrayIP($blacklist))) { $responseMessage = 'User is blacklisted'; $this->setAPIResponse('error', $responseMessage, 401, $userInfo); $this->logger->debug($responseMessage, $userInfo); return true; } } if ($group !== null) { if ((isset($_SERVER['HTTP_X_FORWARDED_SERVER']) && $_SERVER['HTTP_X_FORWARDED_SERVER'] == 'traefik') || $this->config['traefikAuthEnable']) { $return = (isset($_SERVER['HTTP_X_FORWARDED_HOST']) && isset($_SERVER['HTTP_X_FORWARDED_URI']) && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) ? '?return=' . $_SERVER['HTTP_X_FORWARDED_PROTO'] . '://' . $_SERVER['HTTP_X_FORWARDED_HOST'] . $_SERVER['HTTP_X_FORWARDED_URI'] : ''; $redirectDomain = ($this->config['traefikDomainOverride'] !== '') ? $this->config['traefikDomainOverride'] : $this->getServerPath(); $redirect = 'Location: ' . $redirectDomain . $return; } if ($this->qualifyRequest($group) && $unlocked) { header("X-Organizr-User: $currentUser"); header("X-Organizr-Email: $currentEmail"); header("X-Organizr-Group: $currentGroup"); $responseMessage = 'User is authorized'; $this->setAPIResponse('success', $responseMessage, 200, $userInfo); $this->logger->debug($responseMessage, $userInfo); } else { if (!$redirect) { $this->setAPIResponse('error', $responseMessage, 401, $userInfo); $this->logger->debug($responseMessage, $userInfo); } else { exit(http_response_code(401) . header($redirect)); } } } else { $this->setAPIResponse('error', 'Missing info', 401); $this->logger->debug('Missing info', $userInfo); } return true; } else { $this->setAPIResponse('error', 'Organizr is not setup or an error occurred', 401); return false; } } public function getIpInfo($ip = null) { if (!$ip) { $this->setResponse(422, 'No IP Address supplied'); return false; } try { $options = array('verify' => false); $response = Requests::get('https://ipinfo.io/' . $ip . '/?token=ddd0c072ad5021', array(), $options); if ($response->success) { $api = json_decode($response->body, true); $this->setResponse(200, null, $api); return true; } else { $this->setResponse(500, 'An error occurred', null); } } catch (Requests_Exception $e) { $this->setResponse(500, 'An error occurred', $e->getMessage()); } return false; } public function setAPIResponse($result = null, $message = null, $responseCode = null, $data = null) { if ($result) { $GLOBALS['api']['response']['result'] = $result; } if ($message) { $GLOBALS['api']['response']['message'] = $message; } if ($responseCode) { $GLOBALS['responseCode'] = $responseCode; } if ($data) { $GLOBALS['api']['response']['data'] = $data; } } public function setResponse(int $responseCode = 200, string $message = null, $data = null) { switch ($responseCode) { case 200: case 201: case 204: $result = 'success'; break; default: $result = 'error'; break; } $GLOBALS['api']['response']['result'] = $result; if ($message) { $GLOBALS['api']['response']['message'] = $message; } if ($responseCode) { $GLOBALS['responseCode'] = $responseCode; } if ($data) { $GLOBALS['api']['response']['data'] = $data; } } public function printWarningsAndErrors() { if (isset($GLOBALS['api']['response']['exceptions'])) { $this->prettyPrint($GLOBALS['api']['response']['exceptions'], true); } else { $this->prettyPrint('No Errors'); } } public function setAPIErrorResponse($number, $message, $file, $line) { if (!(error_reporting() & $number)) { return; } $exceptions = [ E_ERROR => 'E_ERROR', E_WARNING => 'E_WARNING', E_PARSE => 'E_PARSE', E_NOTICE => 'E_NOTICE', E_CORE_ERROR => 'E_CORE_ERROR', E_CORE_WARNING => 'E_CORE_WARNING', E_COMPILE_ERROR => 'E_COMPILE_ERROR', E_COMPILE_WARNING => 'E_COMPILE_WARNING', E_USER_ERROR => 'E_USER_ERROR', E_USER_WARNING => 'E_USER_WARNING', E_USER_NOTICE => 'E_USER_NOTICE', E_STRICT => 'E_STRICT', E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', E_DEPRECATED => 'E_DEPRECATED', E_USER_DEPRECATED => 'E_USER_DEPRECATED', E_ALL => 'E_ALL' ]; switch ($number) { case E_USER_ERROR: case E_ERROR: case E_CORE_ERROR: case E_COMPILE_ERROR: case E_RECOVERABLE_ERROR: $type = 'errors'; break; case E_USER_WARNING: case E_WARNING: case E_CORE_WARNING: case E_COMPILE_WARNING: $type = 'warnings'; break; case E_USER_NOTICE: case E_PARSE: case E_DEPRECATED: case E_USER_DEPRECATED: case E_NOTICE: $type = 'notice'; break; default: $type = 'other'; break; } if ($this->qualifyRequest(1)) { $count = isset($GLOBALS['api']['response']['exceptions'][$type]) ? count($GLOBALS['api']['response']['exceptions'][$type]) : 0; if ($count <= 10) { $GLOBALS['api']['response']['exceptions'][$type][] = [ 'error' => $exceptions[$number], 'message' => $message, 'file' => $file, 'line' => $line ]; } } $this->handleError($exceptions[$number], $message, $file, $line, $type); } public function setErrorResponse($number, $message, $file, $line) { $error = [ 'error' => $number, 'message' => $message, 'file' => $file, 'line' => $line ]; $this->handleError($number, $message, $file, $line); //$this->prettyPrint($error, true); } public function handleError($number, $message, $file, $line, $type) { $log = false; $error = sprintf('Organizr %s: %s in %s on line %d', $number, $message, $file, $line); error_log($error); if ($this->dev) { $log = true; } else { if ($type == 'errors') { $log = true; } } if ($log && $this->hasDB()) { $this->setLoggerChannel('Server Error')->warning('PHP Error', $error); } } public function checkRoute($request) { $route = '/api/v2/' . explode('api/v2/', $request->getUri()->getPath())[1]; $method = $request->getMethod(); $data = $this->apiData($request); if (!in_array($route, $GLOBALS['bypass'])) { if ($this->isApprovedRequest($method, $data) === false) { $this->setAPIResponse('error', 'Not authorized for current Route: ' . $route, 401); $this->setLoggerChannel('API Security'); $this->logger->notice('Killed Attack From [' . ($_SERVER['HTTP_REFERER'] ?? 'No Referer') . ']'); return false; } } return true; } public function apiData($request, $decode = true) { switch ($request->getMethod()) { case 'POST': if (stripos($request->getHeaderLine('Content-Type'), 'application/json') !== false) { return $decode ? json_decode(file_get_contents('php://input', 'r'), true) : file_get_contents('php://input', 'r'); } else { return $request->getParsedBody(); } default: if (stripos($request->getHeaderLine('Content-Type'), 'application/json') !== false) { return $decode ? json_decode(file_get_contents('php://input', 'r'), true) : file_get_contents('php://input', 'r'); } else { return null; } } } public function getPlugins($returnType = 'all') { if ($this->hasDB()) { switch ($returnType) { case 'enabled': $returnType = 'enabled'; break; case 'disabled': $returnType = 'disabled'; break; default: $returnType = 'all'; } $pluginList = []; foreach ($GLOBALS['plugins'] as $key => $value) { if (strpos($value['license'], $this->config['license']) !== false) { $GLOBALS['plugins'][$key]['enabled'] = $this->config[$value['configPrefix'] . '-enabled'] ?? false; if ($returnType == 'all') { $pluginList[$key] = $GLOBALS['plugins'][$key]; } elseif ($returnType == 'enabled' && $GLOBALS['plugins'][$key]['enabled'] == true) { $pluginList[$key] = $GLOBALS['plugins'][$key]; } elseif ($returnType == 'disabled' && $GLOBALS['plugins'][$key]['enabled'] == false) { $pluginList[$key] = $GLOBALS['plugins'][$key]; } } } asort($pluginList); return $pluginList; } return false; } public function refreshCookieName() { $this->cookieName = $this->config['uuid'] !== '' ? 'organizr_token_' . $this->config['uuid'] : 'organizr_token_temp'; } public function favIcons($rootPath = '') { $favicon = ' '; if ($this->config['favIcon'] !== '' && $rootPath !== '') { $this->config['favIcon'] = str_replace('data/favicon', $rootPath . 'data/favicon', $this->config['favIcon']); } return ($this->config['favIcon'] == '') ? $favicon : $this->config['favIcon']; } public function pluginGlobalList() { $pluginSearch = '-enabled'; $pluginInclude = '-include'; $plugins = array_filter($this->config, function ($k) use ($pluginSearch) { return stripos($k, $pluginSearch) !== false; }, ARRAY_FILTER_USE_KEY); $plugins['includes'] = array_filter($this->config, function ($k) use ($pluginInclude) { return stripos($k, $pluginInclude) !== false; }, ARRAY_FILTER_USE_KEY); return $plugins; } public function googleTracking() { if ($this->config['gaTrackingID'] !== '') { return ' '; } return null; } public function matchBrackets($text, $brackets = 's') { switch ($brackets) { case 's': case 'square': $pattern = '#\[(.*?)\]#'; break; case 'c': case 'curly': $pattern = '#\((.*?)\)#'; break; default: return null; } preg_match($pattern, $text, $match); return $match[1]; } public function languagePacks($encode = false) { $files = array(); foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'langpack' . DIRECTORY_SEPARATOR . "*.json") as $filename) { if (strpos(basename($filename), '[') !== false) { $explode = explode('[', basename($filename)); $files[] = array( 'filename' => basename($filename), 'code' => $explode[0], 'language' => $this->matchBrackets(basename($filename)) ); } } usort($files, function ($a, $b) { return $a['language'] <=> $b['language']; }); return ($encode) ? json_encode($files) : $files; } public function getRootPath() { $count = (count(explode('/', $_SERVER['REQUEST_URI']))) - 2; $rootPath = ''; $rootPath .= str_repeat('../', $count); return $rootPath; } public function setTheme($theme = null, $rootPath = '') { $theme = $theme ?? $this->config['theme']; $themeInformation = $this->validateTheme($theme); if (!$themeInformation) { $themeInformation = $this->defaultThemeInformation()['information']['Organizr']; } return ''; } public function validateTheme($theme = null) { $theme = $theme ?? $this->config['theme']; $information = $this->getAllThemesInformation(); if (isset($information[$theme])) { return $information[$theme]; } else { return null; } } public function getAllThemesInformation() { $organizrThemes = $this->defaultThemeInformation(); $userThemes = $this->userThemeInformation(); if ($userThemes) { return array_merge($organizrThemes['information'], $userThemes); } return $organizrThemes['information']; } public function userThemeInformation() { $themes = $this->config['installedThemes']; if (is_array($themes)) { return $themes; } else { return []; } } public function defaultThemeInformation() { return [ 'files' => ['Blue', 'Organizr'], 'information' => [ 'Blue' => [ 'name' => 'Blue', 'repo' => null, 'version' => '1.0.0', 'path' => 'css/themes' ], 'Organizr' => [ 'name' => 'Organizr', 'repo' => null, 'version' => '1.0.0', 'path' => 'css/themes' ] ] ]; } public function getAllThemes() { $organizrThemes = $this->getOrganizrThemes(); $userThemes = $this->getUserThemes(); if ($userThemes) { return array_merge($organizrThemes, $userThemes); } return $organizrThemes; } public function getOrganizrThemes() { $themes = []; $themeFolder = $this->root . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR . 'themes'; $originalThemes = $this->defaultThemeInformation(); foreach (glob($themeFolder . DIRECTORY_SEPARATOR . '*.css') as $filename) { $file = preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)); if (in_array($file, $originalThemes['files'])) { $themes[] = array( 'name' => $file, 'value' => $file, ); } } return $themes; } public function getUserThemes() { $themes = []; $themeFolder = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'themes'; $userThemesInformation = $this->userThemeInformation(); if (file_exists($themeFolder)) { $directoryIterator = new RecursiveDirectoryIterator($themeFolder, FilesystemIterator::SKIP_DOTS); $iteratorIterator = new RecursiveIteratorIterator($directoryIterator); foreach ($iteratorIterator as $info) { if (stripos($info->getFilename(), '.css') !== false) { $file = preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($info->getFilename())); if (key_exists($file, $userThemesInformation)) { $themes[] = [ 'name' => ucwords(str_replace('_', ' ', $file)), 'value' => $file, ]; } } } } return $themes; } public function setPluginListNameFromConfigPrefix() { foreach ($GLOBALS['plugins'] as $pluginName => $pluginInfo) { $GLOBALS['pluginInfo'][strtolower($pluginInfo['configPrefix'])] = $pluginInfo; $GLOBALS['pluginInfo'][strtolower($pluginName)] = $pluginInfo; } } public function pluginFilesFromDirectory($directory, $webDirectory, $type, $settings = false, $rootPath = '') { $files = ''; if (file_exists($directory)) { $directoryIterator = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS); $iteratorIterator = new RecursiveIteratorIterator($directoryIterator); switch ($type) { case 'js': foreach ($iteratorIterator as $info) { if (pathinfo($info->getPathname(), PATHINFO_EXTENSION) == 'js') { $pluginEnabled = false; $keyOriginal = strtoupper(basename(dirname($info->getPathname()))); $key = str_replace('-SETTINGS', '', $keyOriginal); $continue = false; if ($settings) { if ($info->getFilename() == 'settings.js') { $continue = true; } } else { if ($info->getFilename() !== 'settings.js') { $continue = true; } } switch ($key) { case 'PHP-MAILER': $key = 'PHPMAILER'; break; case 'NGXC': $key = 'ngxc'; break; default: $key = $key; } if (isset($this->config[$key . '-enabled'])) { if ($this->config[$key . '-enabled']) { $pluginEnabled = true; } } if ($pluginEnabled || $settings) { if ($continue) { $version = $GLOBALS['pluginInfo'][strtolower($key)]['version'] ?? $this->fileHash; $files .= ''; } } } } break; case 'css': foreach ($iteratorIterator as $info) { if (pathinfo($info->getPathname(), PATHINFO_EXTENSION) == 'css') { $key = basename(dirname($info->getPathname())); $version = $GLOBALS['pluginInfo'][strtolower($key)]['version'] ?? $this->fileHash; $files .= ''; } } break; default: break; } } return $files; } public function pluginFiles($type, $settings = false, $rootPath = '') { $files = ''; $organizrPlugins = $this->root . DIRECTORY_SEPARATOR . 'api' . DIRECTORY_SEPARATOR . 'plugins'; $files .= $this->pluginFilesFromDirectory($organizrPlugins, 'api/plugins/', $type, $settings); $userPlugins = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins'; $files .= $this->pluginFilesFromDirectory($userPlugins, 'data/plugins/', $type, $settings); return $files; } public function formKey($script = true) { if (isset($this->config['organizrHash'])) { if ($this->config['organizrHash'] !== '') { $hash = password_hash(substr($this->config['organizrHash'], 2, 10), PASSWORD_BCRYPT); return ($script) ? '' : $hash; } } } private function checkPHP() { if (!(version_compare(PHP_VERSION, $this->minimumPHP) >= 0)) { die($this->showHTML('PHP Version', 'Organizr needs PHP Version: ' . $this->minimumPHP . '
You have PHP Version: ' . PHP_VERSION)); } } private function checkWritableDB() { if ($this->hasDB()) { if (isset($this->config['dbLocation']) && isset($this->config['dbName'])) { $db = is_writable($this->config['dbLocation'] . $this->config['dbName']); if (!$db) { //die($this->showHTML('Organizr DB is not writable!', 'Please check permissions and/or disk space')); } } else { die($this->showHTML('Config File Malformed', 'dbLocation and/or dbName is not listed in config.php')); } } } // Create config file in the return syntax public function createConfig($array, $path = null, $nest = 0) { $path = ($path) ? $path : $this->userConfigPath; // Define Initial Value $output = array(); // Sort Items ksort($array); // Update the current config version if (!$nest) { // Inject Current Version $output[] = "\t'configVersion' => '" . (isset($array['apply_CONFIG_VERSION']) ? $array['apply_CONFIG_VERSION'] : $this->version) . "'"; } unset($array['configVersion']); unset($array['apply_CONFIG_VERSION']); // Process Settings foreach ($array as $k => $v) { $allowCommit = true; $item = ''; switch (gettype($v)) { case 'boolean': $item = ($v ? 'true' : 'false'); break; case 'integer': case 'double': case 'NULL': $item = $v; break; case 'string': $item = "'" . str_replace(array('\\', "'"), array('\\\\', "\'"), $v) . "'"; break; case 'array': $item = $this->createConfig($v, false, $nest + 1); break; default: $allowCommit = false; } if ($allowCommit) { $output[] = str_repeat("\t", $nest + 1) . "'$k' => $item"; } } // Build output $output = (!$nest ? "loadConfig(); $current = $this->config; } elseif (is_string($current) && is_file($current)) { $current = $this->loadConfig($current); } // Inject Parts foreach ($new as $k => $v) { $current[$k] = $v; $this->config[$k] = $v; } // Return Create return $this->createConfig($current); } public function removeConfigItem($new, $current = false) { // Get config if not supplied if ($current === false) { $current = $this->config; } elseif (is_string($current) && is_file($current)) { $current = $this->loadConfig($current); } // Inject Parts foreach ($new as $k) { if (isset($current[$k])) { $current['deletedConfigItems'][$k] = $current[$k]; $this->config['deletedConfigItems'][$k] = $current[$k]; } unset($current[$k]); unset($this->config[$k]); } // Return Create return $this->createConfig($current); } public function loadConfig($path = null) { $path = ($path) ? $path : $this->userConfigPath; if (!is_file($path)) { return null; } else { return (array)call_user_func(function () use ($path) { return include($path); }); } } public function fillDefaultConfig($array) { $path = $this->defaultConfigPath; if (is_string($path)) { $loadedDefaults = $this->loadConfig($path); } else { $loadedDefaults = $path; } // Include all plugin config files $folder = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins'; $directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS); $iteratorIterator = new RecursiveIteratorIterator($directoryIterator); foreach ($iteratorIterator as $info) { if ($info->getFilename() == 'config.php') { $loadedDefaults = array_merge($loadedDefaults, $this->loadConfig($info->getPathname())); } } /* * Include all custom Plugin routes */ if (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins')) { $folder = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins'; $directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS); $iteratorIterator = new RecursiveIteratorIterator($directoryIterator); foreach ($iteratorIterator as $info) { if ($info->getFilename() == 'config.php') { $loadedDefaults = array_merge($loadedDefaults, $this->loadConfig($info->getPathname())); } } } return (is_array($loadedDefaults) ? $this->fillDefaultConfig_recurse($array, $loadedDefaults) : false); } public function fillDefaultConfig_recurse($current, $defaults) { foreach ($defaults as $k => $v) { if (!isset($current[$k])) { $current[$k] = $v; } elseif (is_array($current[$k]) && is_array($v)) { $current[$k] = $this->fillDefaultConfig_recurse($current[$k], $v); } } return $current; } public function config($tries = 1) { // Load config or default if (file_exists($this->userConfigPath)) { $config = $this->fillDefaultConfig($this->loadConfig($this->userConfigPath)); } else { $config = $this->fillDefaultConfig($this->loadConfig($this->defaultConfigPath)); } if ((!is_array($config) || !file_exists($this->userConfigPath)) && $tries < 5) { $tries++; return $this->config($tries); } return $config; } public function combineConfig($array) { $this->config = array_merge($this->config, $array); return $this->config; } public function status($action = false) { $status = []; $dependenciesActive = []; $dependenciesInactive = []; $extensions = ['PDO_SQLITE', 'PDO', 'SQLITE3', 'zip', 'cURL', 'openssl', 'simplexml', 'json', 'session', 'filter', 'fileinfo', 'mbstring']; $functions = ['hash', 'fopen', 'fsockopen', 'fwrite', 'fclose', 'readfile']; foreach ($extensions as $check) { if (extension_loaded($check)) { array_push($dependenciesActive, $check); } else { array_push($dependenciesInactive, $check); } } foreach ($functions as $check) { if (function_exists($check)) { array_push($dependenciesActive, $check); } else { array_push($dependenciesInactive, $check); } } $status['writable'] = is_writable(dirname(__DIR__, 2)); $status['minVersion'] = (version_compare(PHP_VERSION, $this->minimumPHP) >= 0); $status['os'] = $this->getOS(); $status['php'] = phpversion(); $status['userConfigPathExists'] = file_exists($this->userConfigPath); if (!($status['minVersion'])) { $status['action'] = 'php'; if ($action) { header($this->getServerPath() . 'api/v2/organizr/error'); exit; } } elseif (count($dependenciesInactive) > 0) { $status['action'] = 'dependencies'; } elseif (!$status['writable']) { $status['action'] = 'permission'; } elseif (!$status['userConfigPathExists']) { $status['action'] = 'wizard'; } else { $status['action'] = 'launch'; if ($action) { echo ''; exit; } } return $status; } public function launch() { $status = array(); $dependenciesActive = array(); $dependenciesInactive = array(); $extensions = array('PDO_SQLITE', 'PDO', 'SQLITE3', 'zip', 'cURL', 'openssl', 'simplexml', 'json', 'session', 'filter', 'fileinfo', 'mbstring'); $functions = array('hash', 'fopen', 'fsockopen', 'fwrite', 'fclose', 'readfile'); foreach ($extensions as $check) { if (extension_loaded($check)) { array_push($dependenciesActive, $check); } else { array_push($dependenciesInactive, $check); } } foreach ($functions as $check) { if (function_exists($check)) { array_push($dependenciesActive, $check); } else { array_push($dependenciesInactive, $check); } } if (!file_exists($this->userConfigPath)) { $status['status'] = 'wizard';//wizard - ok for test } if (count($dependenciesInactive) > 0 || !is_writable(dirname(__DIR__, 2)) || !(version_compare(PHP_VERSION, $this->minimumPHP) >= 0)) { $status['status'] = 'dependencies'; } $status['status'] = ($status['status']) ?? 'ok'; $status['writable'] = is_writable(dirname(__DIR__, 2)) ? 'yes' : 'no'; $status['minVersion'] = (version_compare(PHP_VERSION, $this->minimumPHP) >= 0) ? 'yes' : 'no'; $status['dependenciesActive'] = $dependenciesActive; $status['dependenciesInactive'] = $dependenciesInactive; $status['version'] = $this->version; $status['os'] = $this->getOS(); $status['php'] = phpversion(); $status['php_user'] = get_current_user(); $status['userConfigPath'] = $this->userConfigPath; return $status; } public function hasDB() { return (file_exists($this->userConfigPath)) ?? false; } public function hasCookie() { return ($_COOKIE[$this->cookieName]) ?? false; } public function getGuest() { $guest = array( 'group' => 'Guest', 'group_id' => 999, 'image' => 'plugins/images/groups/guest.png' ); $response = [ array( 'function' => 'fetch', 'query' => 'SELECT * FROM groups WHERE `group_id` = 999' ), ]; return $this->hasDB() ? $this->processQueries($response) : $guest; } public function getSchema() { if ($this->config['driver'] == 'sqlite3') { $response = [ array( 'function' => 'fetchAll', 'query' => 'SELECT name, sql FROM sqlite_master WHERE type=\'table\' ORDER BY name' ), ]; return $this->hasDB() ? $this->processQueries($response) : 'Database not setup yet'; } else { return false; } } public function guestUser() { if ($this->hasDB()) { if ($this->getUserLevel() !== 999) { $guest = array( "token" => null, "tokenDate" => null, "tokenExpire" => null, "username" => "Organizr API", "uid" => $this->guestHash(0, 5), "group" => 'Admin', "groupID" => 0, "email" => null, //"groupImage"=>getGuest()['image'], "image" => $this->getGuest()['image'], "userID" => null, "loggedin" => false, "locked" => false, "tokenList" => null, "authService" => null ); } } $guest = $guest ?? array( "token" => null, "tokenDate" => null, "tokenExpire" => null, "username" => "Guest", "uid" => $this->guestHash(0, 5), "group" => $this->getGuest()['group'], "groupID" => $this->getGuest()['group_id'], "email" => null, //"groupImage"=>getGuest()['image'], "image" => $this->getGuest()['image'], "userID" => null, "loggedin" => false, "locked" => false, "tokenList" => null, "authService" => null ); return $guest; } public function getAllUserTokens($id, $includeAllFields = true) { $select = $includeAllFields ? '*' : 'token, ip, id, expires, created'; $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT ' . $select . ' FROM `tokens` WHERE user_id = ? AND expires > ?', [$id], [$this->currentTime] ) ), ]; return $this->processQueries($response); } public function getUserById($id) { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM users WHERE id = ?', $id ) ) ]; return $this->processQueries($response); } public function getUserByEmail($email) { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM users WHERE email = ? COLLATE NOCASE', $email ) ) ]; return $this->processQueries($response); } protected function invalidToken($token) { if (isset($_COOKIE[$this->cookieName])) { if ($token == $_COOKIE[$this->cookieName]) { $this->setLoggerChannel('Authentication'); $this->logger->debug('Token was invalid - deleting cookie and user session'); $this->coookie('delete', $this->cookieName); $this->user = null; } } } public function validateToken($token, $api = false) { // Validate script $userInfo = $this->jwtParse($token); $validated = (bool)$userInfo; if ($validated == true) { $allTokens = $this->getAllUserTokens($userInfo['userID']); $user = $this->getUserById($userInfo['userID']); $tokenKey = $this->searchArray($allTokens, 'token', $token); $tokenCheck = ($tokenKey !== false); if (!$tokenCheck) { $this->setLoggerChannel('Authentication'); $this->logger->debug('Token failed check against all token listings', $allTokens); $this->invalidToken($token); if ($api) { $this->setResponse(403, 'Token was not in approved list'); } return false; } else { // Check if user is on same browser as token if ($allTokens[$tokenKey]['browser'] !== $_SERVER ['HTTP_USER_AGENT']) { if ($this->config['matchUserAgents']) { $this->setLoggerChannel('Authentication')->warning('Mismatch of useragent', ['token' => $allTokens[$tokenKey]['browser'], 'browser' => $_SERVER ['HTTP_USER_AGENT']]); $this->invalidToken($token); return false; } } if (($allTokens[$tokenKey]['ip'] !== $this->userIP()) && (!$this->isLocalOrServer())) { if ($this->config['matchUserIP']) { $this->setLoggerChannel('Authentication')->warning('Mismatch of user IP', ['token' => $allTokens[$tokenKey]['ip'], 'user' => $this->userIP()]); $this->invalidToken($token); return false; } } if ($api) { $this->setResponse(200, 'Token is valid'); } return array( 'token' => $token, 'tokenDate' => $userInfo['tokenDate'], 'tokenExpire' => $userInfo['tokenExpire'], 'username' => $user['username'] ?? $userInfo['username'], 'uid' => $this->guestHash(0, 5), 'group' => $user['group'] ?? $userInfo['group'], 'groupID' => $user['group_id'] ?? $userInfo['groupID'], 'email' => $user['email'] ?? $userInfo['email'], 'image' => $user['image'] ?? $userInfo['image'], 'userID' => $user['id'] ?? $userInfo['userID'], 'loggedin' => true, 'locked' => $user['locked'] ?? 0, 'tokenList' => $allTokens, 'authService' => (isset($user['auth_service'])) ? explode('::', $user['auth_service'])[0] : 'internal' ); } } else { if ($api) { $this->setResponse(403, 'Token was invalid'); } $this->setLoggerChannel('Authentication'); $this->logger->debug('User token was invalid', ['token' => $token]); $this->invalidToken($token); } if ($api) { $this->setResponse(403, 'Token was invalid'); } return false; } public function getUserFromToken($token) { // Validate script $userInfo = $this->jwtParse($token); $validated = (bool)$userInfo; if ($validated == true) { $user = $this->getUserById($userInfo['userID']); $allTokens = $this->getAllUserTokens($userInfo['userID'], false); return array( 'token' => $token, 'tokenDate' => $userInfo['tokenDate'], 'tokenExpire' => $userInfo['tokenExpire'], 'username' => $user['username'] ?? $userInfo['username'], 'uid' => $this->guestHash(0, 5), 'group' => $user['group'] ?? $userInfo['group'], 'groupID' => $user['group_id'] ?? $userInfo['groupID'], 'email' => $user['email'] ?? $userInfo['email'], 'image' => $user['image'] ?? $userInfo['image'], 'userID' => $user['id'] ?? $userInfo['userID'], 'loggedin' => true, 'locked' => $user['locked'] ?? 0, 'tokenList' => $allTokens, 'authService' => (isset($user['auth_service'])) ? explode('::', $user['auth_service'])[0] : 'internal' ); } return false; } public function defaultUserGroup() { $response = [ array( 'function' => 'fetch', 'query' => 'SELECT * FROM groups WHERE `default` = 1' ) ]; return $this->processQueries($response); } public function getAllTabs() { $response = [ array( 'function' => 'fetchAll', 'query' => 'SELECT * FROM tabs ORDER BY `order` ASC', 'key' => 'tabs' ), array( 'function' => 'fetchAll', 'query' => 'SELECT * FROM categories ORDER BY `order` ASC', 'key' => 'categories' ), array( 'function' => 'fetchAll', 'query' => 'SELECT * FROM groups ORDER BY `group_id` ASC', 'key' => 'groups' ), ]; $query = $this->processQueries($response); $this->applyTabVariables($query['tabs']); return $query; } public function applyTabVariables($tabs) { $variables = [ '{domain}' => $this->getServer(), '{username}' => $this->user['username'], '{username_lower}' => $this->user['username'], '{email}' => $this->user['email'], '{group}' => $this->user['group'], '{group_id}' => $this->user['groupID'], '{komga}' => $_COOKIE['komga_token'] ?? '' ]; if (empty($tabs)) { return $tabs; } foreach ($tabs as $id => $tab) { $tabs[$id]['url'] = $this->userDefinedIdReplacementLink($tab['url'], $variables); $tabs[$id]['url_local'] = $this->userDefinedIdReplacementLink($tab['url_local'], $variables); } return $tabs; } public function getUsers() { $response = [ array( 'function' => 'fetchAll', 'query' => 'SELECT * FROM users' ), array( 'function' => 'fetchAll', 'query' => 'SELECT * FROM groups ORDER BY group_id ASC' ), ]; return $this->processQueries($response); } public function usernameTaken($username, $email, $id = null) { if ($id) { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM users WHERE `id` != ? AND (username = ? COLLATE NOCASE or email = ? COLLATE NOCASE)', $id, $username, $email ) ), ]; } else { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM users WHERE username = ? COLLATE NOCASE or email = ? COLLATE NOCASE', [$username], [$email] ) ), ]; } return $this->processQueries($response); } public function cleanPageName($page) { return ($page) ? strtolower(str_replace(array('%20', ' ', '-', '_'), '_', $page)) : ''; } public function cleanClassName($name, $char = '-') { return ($name) ? (str_replace(array('%20', ' ', '-', '_'), $char, strtolower($name))) : ''; } public function reverseCleanClassName($name) { return ($name) ? (str_replace(array('%20', '-', '_'), ' ', strtolower($name))) : ''; } public function getPageList() { return $GLOBALS['organizrPages']; } public function getPage($page) { if (!$page) { $this->setAPIResponse('error', 'Page not setup', 409); return null; } $pageFunction = 'get_page_' . $this->cleanPageName($page); if (function_exists($pageFunction)) { return $pageFunction($this); } else { $this->setAPIResponse('error', 'Page not setup', 409); return null; } } public function getUserLevel() { // Grab token $requesterToken = $this->getallheadersi()['token'] ?? ($_GET['apikey'] ?? false); $apiKey = ($this->config['organizrAPI']) ?? null; // Check token or API key // If API key, return 0 for admin if (strlen($requesterToken) == 20 && $requesterToken == $apiKey) { //DO API CHECK return 0; } elseif (isset($this->user)) { return $this->user['groupID']; } // All else fails? return guest id return 999; } public function qualifyRequest($accessLevelNeeded, $api = false) { if ($this->getUserLevel() <= $accessLevelNeeded && $this->getUserLevel() !== null) { return true; } else { if ($api) { $this->setAPIResponse('error', 'Not Authorized', 401); } return false; } } public function qualifyLength($string, $length = 100, $api = false) { if (strlen($string) <= $length) { return true; } else { if ($api) { $this->setResponse(409, 'String is over limit of: ' . $length); } return false; } } public function getImages() { $allIconsPrep = array(); $allIcons = array(); $ignore = array(".", "..", "._.DS_Store", ".DS_Store", ".pydio_id", "index.html"); $dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'tabs' . DIRECTORY_SEPARATOR; $path = 'plugins/images/tabs/'; $images = scandir($dirname); foreach ($images as $image) { if (!in_array($image, $ignore)) { $allIconsPrep[$image] = array( 'path' => $path, 'name' => $image ); } } $dirname = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR; $path = 'data/userTabs/'; if (file_exists($dirname)) { $images = scandir($dirname); foreach ($images as $image) { if (!in_array($image, $ignore)) { $allIconsPrep[$image] = array( 'path' => $path, 'name' => $image ); } } } uksort($allIconsPrep, 'strcasecmp'); foreach ($allIconsPrep as $item) { $allIcons[] = $item['path'] . $item['name']; } return $allIcons; } public function getImagesSelect() { $term = $_GET['search'] ?? null; $page = $_GET['page'] ?? 1; $limit = $_GET['limit'] ?? 20; $offset = ($page * $limit) - $limit; $goodIcons['results'] = []; $goodIcons['limit'] = $limit; $goodIcons['page'] = $page; $goodIcons['term'] = $term; $imageListing = $this->getImages(); $newImageListing = []; foreach ($imageListing as $image) { $newImageListing[] = [ 'id' => $image, 'text' => basename($image) ]; } foreach ($newImageListing as $k => $v) { if ($term) { if (stripos($v['text'], $term) !== false) { $goodIcons['results'][] = $v; } } else { $goodIcons['results'][] = $v; } } $total = count($goodIcons['results']); $goodIcons['total'] = $total; $goodIcons['results'] = array_slice($goodIcons['results'], $offset, $limit); $goodIcons['pagination']['more'] = $page < (ceil($total / $limit)); return $goodIcons; } public function removeImage($image = null) { if (!$image) { $this->setAPIResponse('error', 'No image supplied', 422); return false; } $approvedPath = 'data/userTabs/'; $removeImage = $approvedPath . pathinfo($image, PATHINFO_BASENAME); if ($this->approvedFileExtension($removeImage, 'image')) { if (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . $removeImage)) { $this->setLoggerChannel('Image Manager'); $this->logger->info('Image Manager Function - Deleted Image [' . pathinfo($image, PATHINFO_BASENAME) . ']'); $this->setAPIResponse(null, pathinfo($image, PATHINFO_BASENAME) . ' has been deleted', null); return (unlink(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . $removeImage)); } else { $this->setAPIResponse('error', $removeImage . ' does not exist', 404); return false; } } else { $this->setAPIResponse('error', $removeImage . ' is not approved to be deleted', 409); return false; } } public function uploadImage() { $filesCheck = array_filter($_FILES); if (empty($filesCheck)) { $this->setResponse(500, 'No File was uploaded'); return false; } if ($_FILES['file']['tmp_name'] == '') { $this->setResponse(500, 'File upload error'); return false; } if (strpos($_FILES['file']['type'], 'image/') === false) { $this->setResponse(403, 'File Type not image', $_FILES['file']['type']); return false; } if (!$this->approvedFileType($_FILES['file']['tmp_name'])) { $this->setResponse(403, 'File Type not approved', $_FILES['file']['tmp_name']); return false; } if (!$this->approvedFileExtension($_FILES['file']['name'])) { $this->setResponse(403, 'File Extension not approved'); return false; } ini_set('upload_max_filesize', '10M'); ini_set('post_max_size', '10M'); $tempFile = $_FILES['file']['tmp_name']; $targetPath = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR; $this->makeDir($targetPath); $targetFile = $targetPath . $this->sanitizeUserString($_FILES['file']['name']); $this->setAPIResponse(null, pathinfo($_FILES['file']['name'], PATHINFO_BASENAME) . ' has been uploaded', null); return move_uploaded_file($tempFile, $targetFile); } public function formatPingHost($host) { $host = $this->qualifyURL($host, true); if ($host['port'] !== '') { $host['port'] = str_replace(':', '', $host['port']); } if ($host['host'] == '' && $host['path'] !== '') { $host['host'] = $host['path']; $host['path'] = ''; if (strpos($host['host'], '/') !== false) { $host['host'] = explode('/', $host['host'])[0]; } } return $host; } public function ping($pings) { if ($this->qualifyRequest($this->config['pingAuth'], true)) { if (!$pings['list']) { $this->setAPIResponse('error', 'No ping hostname/IP\'s entered', 409); return null; } $pings = $pings['list']; $type = gettype($pings); $ping = new Ping(""); $ping->setTtl(128); $ping->setTimeout(2); switch ($type) { case "array": $results = []; foreach ($pings as $k => $v) { $pingFormatted = $this->formatPingHost($v); $ping->setHost($pingFormatted['host']); if ($pingFormatted['port'] !== '') { $ping->setPort($pingFormatted['port']); $latency = $ping->ping('fsockopen'); } else { $latency = $ping->ping(); } if ($latency || $latency === 0) { $results[$v] = $latency; } else { $results[$v] = false; } } break; case "string": $pingFormatted = $this->formatPingHost($pings); $ping->setHost($pingFormatted['host']); if ($pingFormatted['port'] !== '') { $ping->setPort($pingFormatted['port']); $latency = $ping->ping('fsockopen'); } else { $latency = $ping->ping(); } if ($latency || $latency === 0) { $results = $latency; } else { $results = null; } break; } return ($results) ?? null; } return null; } public function getPluginSettings() { return [ 'Marketplace' => [ $this->settingsOption('notice', null, ['notice' => 'danger', 'body' => '3rd Party Repositories are not affiliated with Organizr and therefore the code on these repositories are not inspected. Use at your own risk.']), $this->settingsOption('multiple-url', 'externalPluginMarketplaceRepos', ['override' => 12, 'label' => 'External Marketplace Repo', 'help' => 'Only supports Github repos']), $this->settingsOption('token', 'githubAccessToken', ['label' => 'Github Person Access Token', 'help' => 'The Github Person Access Token will help with API rate limiting as well as let you access your own Private Repos']), $this->settingsOption('switch', 'checkForPluginUpdate', ['label' => 'Check for Plugin Updates', ['help' => 'Check for updates on page load']]) ] ]; } public function getThemeSettings() { return [ 'Marketplace' => [ $this->settingsOption('notice', null, ['notice' => 'danger', 'body' => '3rd Party Repositories are not affiliated with Organizr and therefore the themes on these repositories are not inspected. Use at your own risk.']), $this->settingsOption('multiple-url', 'externalThemeMarketplaceRepos', ['override' => 12, 'label' => 'External Marketplace Repo', 'help' => 'Only supports Github repos']), $this->settingsOption('token', 'githubAccessToken', ['label' => 'Github Person Access Token', 'help' => 'The Github Person Access Token will help with API rate limiting as well as let you access your own Private Repos']), $this->settingsOption('switch', 'checkForThemeUpdate', ['label' => 'Check for Theme Updates', ['help' => 'Check for updates on page load']]) ] ]; } public function getCustomizeAppearance() { return [ 'Top Bar' => [ $this->settingsOption('input', 'logo', ['label' => 'Logo URL']), $this->settingsOption('input', 'title', ['label' => 'Organizr Title']), $this->settingsOption('switch', 'useLogo', ['label' => 'Use Logo instead of Title', 'help' => 'Also sets the title of your site']), $this->settingsOption('input', 'description', ['label' => 'Meta Description', 'help' => 'Used to set the description for SEO meta tags']), ], 'Side Menu' => [ $this->settingsOption('switch', 'allowCollapsableSideMenu', ['label' => 'Allow Side Menu to be Collapsable']), $this->settingsOption('switch', 'sideMenuCollapsed', ['label' => 'Side Menu Collapsed at Launch']), $this->settingsOption('switch', 'collapseSideMenuOnClick', ['label' => 'Collapse Side Menu after clicking Tab']), $this->settingsOption('switch', 'githubMenuLink', ['label' => 'Show GitHub Repo Link']), $this->settingsOption('switch', 'organizrFeatureRequestLink', ['label' => 'Show Organizr Feature Request Link']), $this->settingsOption('switch', 'organizrSupportMenuLink', ['label' => 'Show Organizr Support Link']), $this->settingsOption('switch', 'organizrDocsMenuLink', ['label' => 'Show Organizr Docs Link']), $this->settingsOption('switch', 'organizrSignoutMenuLink', ['label' => 'Show Organizr Sign out & in Button on Sidebar']), $this->settingsOption('switch', 'expandCategoriesByDefault', ['label' => 'Expand All Categories']), $this->settingsOption('switch', 'autoCollapseCategories', ['label' => 'Auto-Collapse Categories']), $this->settingsOption('switch', 'autoExpandNavBar', ['label' => 'Auto-Expand Nav Bar']), $this->settingsOption('select', 'unsortedTabs', ['label' => 'Unsorted Tab Placement', 'options' => [['name' => 'Top', 'value' => 'top'], ['name' => 'Bottom', 'value' => 'bottom']]]), ], 'Login Page' => [ $this->settingsOption('input', 'loginLogo', ['label' => 'Login Logo URL']), $this->settingsOption('multiple-url', 'loginWallpaper', ['label' => 'Login Wallpaper URL', 'help' => 'You may enter multiple URL\'s']), $this->settingsOption('switch', 'useLogoLogin', ['label' => 'Use Logo instead of Title on Login Page']), $this->settingsOption('switch', 'minimalLoginScreen', ['label' => 'Minimal Login Screen']), $this->settingsOption('switch', 'useRandomMediaImage', ['label' => 'Use Random Media Wallpaper From Media Server']), ], 'Options' => [ $this->settingsOption('switch', 'alternateHomepageHeaders', ['label' => 'Alternate Homepage Titles']), $this->settingsOption('switch', 'disableHomepageModals', ['label' => 'Disable Homepage Saved Modal', 'help' => 'Disable the modal when saving homepage config settings']), $this->settingsOption('switch', 'debugErrors', ['label' => 'Show Debug Errors']), $this->settingsOption('switch', 'easterEggs', ['label' => 'Show Easter Eggs']), $this->settingsOption('input', 'gaTrackingID', ['label' => 'Google Analytics Tracking ID', 'placeholder' => 'e.g. UA-XXXXXXXXX-X']), ], 'Colors & Themes' => [ $this->settingsOption('notice', null, ['notice' => 'info', 'title' => 'Attention', 'bodyHTML' => 'The value of #987654 is just a placeholder, you can change to any value you like.To revert back to default, save with no value defined in the relevant field.']), $this->settingsOption('blank'), $this->settingsOption('button', '', ['label' => 'Reset Colors', 'icon' => 'fa fa-ticket', 'text' => 'Reset', 'attr' => 'onclick="resetCustomColors()"']), $this->settingsOption('blank'), $this->settingsOption('color', 'headerColor', ['label' => 'Nav Bar Color']), $this->settingsOption('color', 'headerTextColor', ['label' => 'Nav Bar Text Color']), $this->settingsOption('color', 'sidebarColor', ['label' => 'Side Bar Color']), $this->settingsOption('color', 'sidebarTextColor', ['label' => 'Side Bar Text Color']), $this->settingsOption('color', 'accentColor', ['label' => 'Accent Color']), $this->settingsOption('color', 'accentTextColor', ['label' => 'Accent Text Color']), $this->settingsOption('color', 'buttonColor', ['label' => 'Button Color']), $this->settingsOption('color', 'buttonTextColor', ['label' => 'Button Text Color']), $this->settingsOption('select', 'theme', ['label' => 'Theme', 'class' => 'themeChanger', 'options' => $this->getAllThemes()]), $this->settingsOption('select', 'style', ['label' => 'Style', 'class' => 'styleChanger', 'options' => [['name' => 'Light', 'value' => 'light'], ['name' => 'Dark', 'value' => 'dark'], ['name' => 'Horizontal', 'value' => 'horizontal']]]), ], 'Notifications' => [ $this->settingsOption('select', 'notificationBackbone', ['label' => 'Type', 'class' => 'notifyChanger', 'options' => $this->notificationTypesOptions()]), $this->settingsOption('select', 'notificationPosition', ['label' => 'Position', 'class' => 'notifyPositionChanger', 'options' => $this->notificationPositionsOptions()]), $this->settingsOption('html', null, ['label' => 'Test Message', 'html' => '
'] ), ], 'FavIcon' => [ $this->settingsOption('html', null, ['label' => 'Instructions', 'override' => 12, 'html' => '
Visit FavIcon Site
  • Click [Select your Favicon picture]
  • Choose your image to use
  • Edit settings to your liking
  • At bottom of page on [Favicon Generator Options] under [Path] choose [I cannot or I do not want to place favicon files at the root of my web site.]
  • Enter this path data/favicon
  • Click [Generate your Favicons and HTML code]
  • Download and unzip file and place in data/favicon
  • Copy code and paste inside left box
'] ), $this->settingsOption('code-editor', 'favIcon', ['label' => 'Fav Icon Code', 'mode' => 'html']), ], 'Custom CSS' => [ $this->settingsOption('code-editor', 'customCss', ['label' => 'Custom CSS', 'mode' => 'css']), ], 'Theme CSS' => [ $this->settingsOption('code-editor', 'customThemeCss', ['label' => 'Theme CSS', 'mode' => 'css']), ], 'Custom Javascript' => [ $this->settingsOption('code-editor', 'customJava', ['label' => 'Custom Javascript', 'mode' => 'javascript']), ], 'Theme Javascript' => [ $this->settingsOption('code-editor', 'customThemeJava', ['label' => 'Theme Javascript', 'mode' => 'javascript']), ], ]; } public function loadAppearance() { $appearance['logo'] = $this->config['logo']; $appearance['title'] = $this->config['title']; $appearance['useLogo'] = $this->config['useLogo']; $appearance['useLogoLogin'] = $this->config['useLogoLogin']; $appearance['headerColor'] = $this->config['headerColor']; $appearance['headerTextColor'] = $this->config['headerTextColor']; $appearance['sidebarColor'] = $this->config['sidebarColor']; $appearance['headerTextColor'] = $this->config['headerTextColor']; $appearance['sidebarTextColor'] = $this->config['sidebarTextColor']; $appearance['accentColor'] = $this->config['accentColor']; $appearance['accentTextColor'] = $this->config['accentTextColor']; $appearance['buttonColor'] = $this->config['buttonColor']; $appearance['buttonTextColor'] = $this->config['buttonTextColor']; $appearance['buttonTextHoverColor'] = $this->config['buttonTextHoverColor']; $appearance['buttonHoverColor'] = $this->config['buttonHoverColor']; $appearance['loginWallpaper'] = $this->config['loginWallpaper']; $appearance['randomMediaImage'] = $this->getRandomMediaImage('np'); $appearance['loginLogo'] = $this->config['loginLogo']; $appearance['customCss'] = $this->config['customCss']; $appearance['customThemeCss'] = $this->config['customThemeCss']; $appearance['customJava'] = $this->config['customJava']; $appearance['customThemeJava'] = $this->config['customThemeJava']; return $appearance; } public function getRandomMediaImage($type = null) { if (!$this->config['useRandomMediaImage']) { return false; } if (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cache')) { $folder = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cache'; try { $directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS); $iteratorIterator = new RecursiveIteratorIterator($directoryIterator); $image = null; switch ($type) { case 'np': $i = 0; $array = iterator_to_array($iteratorIterator); if (count($array) > 0) { shuffle($array); $iteratorIterator = new ArrayIterator($array); } foreach ($iteratorIterator as $info) { if (stripos($info->getFilename(), 'np') !== false) { if ($i < 1) { $imageInfo = getimagesize($folder . DIRECTORY_SEPARATOR . $info->getFilename()); if ($imageInfo[0] >= $this->getCacheImageSize('npw')) { $image = 'data/cache/' . $info->getFilename(); $i++; } } else { break; } } } return $image; default: return false; } } catch (Exception $e) { return false; } } else { return false; } } public function getSettingsMain() { $certificateStatus = $this->hasCustomCert() ? 'Custom Certificate Loaded
Located at ' . $this->getCustomCert() . '' : 'Custom Certificate not found - please upload below'; $settings = [ 'Socks' => [ $this->settingsOption('switch', 'socksDebug', ['label' => 'Enable Debug Log', 'help' => 'Enable the option to have socks output to logs']), $this->settingsOption('number', 'maxSocksDebugSize', ['label' => 'Max Debug Data Rows', 'help' => 'Max amount of rows in debug log', 'attr' => 'min="1"']) ], 'Settings Page' => [ $this->settingsOption('select', 'defaultSettingsTab', ['label' => 'Default Settings Tab', 'options' => $this->getSettingsTabs(), 'help' => 'Choose which Settings Tab to be default when opening settings page']), ], 'Other' => [ $this->settingsOption('switch', 'checkForUpdate', ['label' => 'Check For Update', 'help' => 'Check for update on Organizr load']), ], 'Github' => [ $this->settingsOption('select', 'branch', ['label' => 'Branch', 'value' => $this->config['branch'], 'options' => $this->getBranches(), 'disabled' => $this->docker, 'help' => ($this->docker) ? 'Since you are using the Official Docker image, Change the image to change the branch' : 'Choose which branch to download from']), $this->settingsOption('button', 'force-install-branch', ['label' => 'Force Install Branch', 'class' => 'updateNow', 'icon' => 'fa fa-download', 'text' => 'Retrieve', 'attr' => ($this->docker) ? 'title="You can just restart your docker to update"' : '', 'help' => ($this->docker) ? 'Since you are using the official Docker image, you can just restart your Docker container to update Organizr' : 'This will re-download all of the source files for Organizr']), ], 'API' => [ $this->settingsOption('password-alt-copy', 'organizrAPI', ['label' => 'Organizr API']), $this->settingsOption('button', null, ['label' => 'Generate New API Key', 'class' => 'newAPIKey', 'icon' => 'fa fa-refresh', 'text' => 'Generate']), $this->settingsOption('notice', null, ['title' => 'API Documentation', 'body' => 'The documentation for Organizr\'s API is included with this installation. To access the docs, use the button below.', 'bodyHTML' => '

Organizr Docs
']) ], 'Authentication' => [ $this->settingsOption('select', 'authType', ['id' => 'authSelect', 'label' => 'Authentication Type', 'value' => $this->config['authType'], 'options' => $this->getAuthTypes()]), $this->settingsOption('select', 'authBackend', ['id' => 'authBackendSelect', 'label' => 'Authentication Backend', 'class' => 'backendAuth switchAuth', 'value' => $this->config['authBackend'], 'options' => $this->getAuthBackends()]), $this->settingsOption('token', 'plexToken', ['class' => 'plexAuth switchAuth']), $this->settingsOption('button', '', ['class' => 'getPlexTokenAuth plexAuth switchAuth', 'label' => 'Get Plex Token', 'icon' => 'fa fa-ticket', 'text' => 'Retrieve', 'attr' => 'onclick="PlexOAuth(oAuthSuccess,oAuthError, null, \'#settings-main-form [name=plexToken]\')"']), $this->settingsOption('password-alt', 'plexID', ['class' => 'plexAuth switchAuth', 'label' => 'Plex Machine', 'placeholder' => 'Use Get Plex Machine Button']), $this->settingsOption('button', '', ['class' => 'getPlexMachineAuth plexAuth switchAuth', 'label' => 'Get Plex Machine', 'icon' => 'fa fa-id-badge', 'text' => 'Retrieve', 'attr' => 'onclick="showPlexMachineForm(\'#settings-main-form [name=plexID]\')"']), $this->settingsOption('input', 'plexAdmin', ['label' => 'Plex Admin Username or Email', 'class' => 'plexAuth switchAuth', 'placeholder' => 'Admin username for Plex']), $this->settingsOption('switch', 'plexoAuth', ['label' => 'Enable Plex oAuth', 'class' => 'plexAuth switchAuth']), $this->settingsOption('switch', 'ignoreTFAIfPlexOAuth', ['label' => 'Ignore 2FA if Plex OAuth ', 'class' => 'plexAuth switchAuth', 'help' => 'Enabling this will disable Organizr 2FA (If applicable) if User uses Plex OAuth to login']), $this->settingsOption('switch', 'plexStrictFriends', ['label' => 'Strict Plex Friends ', 'class' => 'plexAuth switchAuth', 'help' => 'Enabling this will only allow Friends that have shares to the Machine ID entered above to login, Having this disabled will allow all Friends on your Friends list to login']), $this->settingsOption('switch', 'ignoreTFALocal', ['label' => 'Ignore External 2FA on Local Subnet', 'help' => 'Enabling this will bypass external 2FA security if user is on local Subnet']), $this->settingsOption('url', 'authBackendHost', ['class' => 'ldapAuth ftpAuth switchAuth', 'label' => 'Host Address', 'placeholder' => 'http(s) | ftp(s) | ldap(s)://hostname:port']), $this->settingsOption('input', 'authBaseDN', ['class' => 'ldapAuth switchAuth', 'label' => 'Host Base DN', 'placeholder' => 'cn=%s,dc=sub,dc=domain,dc=com']), $this->settingsOption('input', 'authBackendHostPrefix', ['class' => 'ldapAuth switchAuth', 'label' => 'Account Prefix', 'id' => 'authBackendHostPrefix-input', 'placeholder' => 'Account prefix - i.e. Controller\ from Controller\Username for AD - uid= for OpenLDAP']), $this->settingsOption('input', 'authBackendHostSuffix', ['class' => 'ldapAuth switchAuth', 'label' => 'Account Suffix', 'id' => 'authBackendHostSuffix-input', 'placeholder' => 'Account suffix - start with comma - ,ou=people,dc=domain,dc=tld']), $this->settingsOption('input', 'ldapBindUsername', ['class' => 'ldapAuth switchAuth', 'label' => 'Bind Username']), $this->settingsOption('password', 'ldapBindPassword', ['class' => 'ldapAuth switchAuth', 'label' => 'Bind Password']), $this->settingsOption('select', 'ldapType', ['id' => 'ldapType', 'label' => 'LDAP Backend Type', 'class' => 'ldapAuth switchAuth', 'options' => $this->getLDAPOptions()]), $this->settingsOption('html', null, ['class' => 'ldapAuth switchAuth', 'label' => 'Account DN', 'html' => '' . $this->config['authBackendHostPrefix'] . 'TestAcct' . $this->config['authBackendHostSuffix'] . '']), $this->settingsOption('blank', null, ['class' => 'ldapAuth switchAuth']), $this->settingsOption('switch', 'ldapSSL', ['class' => 'ldapAuth switchAuth', 'label' => 'Enable LDAP SSL', 'help' => 'This will enable the use of SSL for LDAP connections']), $this->settingsOption('switch', 'ldapSSL', ['class' => 'ldapAuth switchAuth', 'label' => 'Enable LDAP TLS', 'help' => 'This will enable the use of TLS for LDAP connections']), $this->settingsOption('test', 'ldap', ['class' => 'ldapAuth switchAuth']), $this->settingsOption('test', '', ['label' => 'Test Login', 'class' => 'ldapAuth switchAuth', 'text' => 'Test Login', 'attr' => 'onclick="showLDAPLoginTest()"']), $this->settingsOption('url', 'embyURL', ['class' => 'embyAuth switchAuth', 'label' => 'Emby URL', 'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.']), $this->settingsOption('token', 'embyToken', ['class' => 'embyAuth switchAuth', 'label' => 'Emby Token']), $this->settingsOption('url', 'jellyfinURL', ['class' => 'jellyfinAuth switchAuth', 'label' => 'Jellyfin URL', 'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.']), $this->settingsOption('token', 'jellyfinToken', ['class' => 'jellyfinAuth switchAuth', 'label' => 'Jellyfin Token']), ], 'Security' => [ $this->settingsOption('number', 'loginAttempts', ['label' => 'Max Login Attempts']), $this->settingsOption('select', 'loginLockout', ['label' => 'Login Lockout Seconds', 'options' => $this->timeOptions()]), $this->settingsOption('number', 'lockoutTimeout', ['label' => 'Inactivity Timer [Minutes]']), $this->settingsOption('switch', 'lockoutSystem', ['label' => 'Inactivity Lock']), $this->settingsOption('select', 'lockoutMinAuth', ['label' => 'Lockout Groups From', 'options' => $this->groupSelect()]), $this->settingsOption('select', 'lockoutMaxAuth', ['label' => 'Lockout Groups To', 'options' => $this->groupSelect()]), $this->settingsOption('switch', 'matchUserAgents', ['label' => 'Match UserAgent', 'help' => 'Match Browser UserAgent to Token UserAgent - Can be very aggressive on matching']), $this->settingsOption('switch', 'matchUserIP', ['label' => 'Match User IP', 'help' => 'Match User IP to Token IP - Also allows approval if user token is valid and is local to server']), $this->settingsOption('switch', 'traefikAuthEnable', ['label' => 'Enable Traefik Auth Redirect', 'help' => 'This will enable the webserver to forward errors so traefik will accept them']), $this->settingsOption('input', 'traefikDomainOverride', ['label' => 'Traefik Domain for Return Override', 'help' => 'Please use a FQDN on this URL Override', 'placeholder' => 'http(s)://domain']), $this->settingsOption('select', 'debugAreaAuth', ['label' => 'Minimum Authentication for Debug Area', 'options' => $this->groupSelect(), 'settings' => '{}']), $this->settingsOption('multiple', 'sandbox', ['override' => 12, 'label' => 'iFrame Sandbox', 'help' => 'WARNING! This can potentially mess up your iFrames', 'options' => $this->sandboxOptions()]), $this->settingsOption('multiple', 'iframeAllow', ['override' => 12, 'label' => 'iFrame Allow', 'help' => 'WARNING! This can potentially mess up your iFrames', 'options' => $this->iframeAllowOptions()]), $this->settingsOption('multiple', 'blacklisted', ['override' => 12, 'label' => 'Blacklisted IP\'s', 'help' => 'WARNING! This will block anyone with these IP\'s', 'options' => $this->makeOptionsFromValues($this->config['blacklisted']), 'settings' => '{tags: true}']), $this->settingsOption('code-editor', 'blacklistedMessage', ['mode' => 'html']), ], 'Logs' => [ $this->settingsOption('folder', 'logLocation', ['label' => 'Log Save Path', 'help' => 'Folder path to save Organizr Logs - Please test before saving', 'value' => $this->logLocation()]), $this->settingsOption('select', 'logLevel', ['label' => 'Log Level', 'options' => $this->logLevels()]), $this->settingsOption('switch', 'includeDatabaseQueriesInDebug', ['label' => 'Include Database Queries', 'help' => 'Include Database queries in debug logs']), $this->settingsOption('number', 'maxLogFiles', ['label' => 'Maximum Log Files', 'help' => 'Number of log files to preserve', 'attr' => 'min="1"']), $this->settingsOption('select', 'logLiveUpdateRefresh', ['label' => 'Live Update Refresh', 'options' => $this->timeOptions()]), $this->settingsOption('select', 'logPageSize', ['label' => 'Log Page Size', 'options' => [['name' => '10 Items', 'value' => '10'], ['name' => '25 Items', 'value' => '25'], ['name' => '50 Items', 'value' => '50'], ['name' => '100 Items', 'value' => '100']]]), $this->settingsOption('switch', 'sendLogsToSlack', ['label' => 'Send Logs to Slack', 'help' => 'Send Logs to Slack as well']), $this->settingsOption('select', 'slackLogLevel', ['label' => 'Slack Log Level', 'options' => $this->logLevels()]), $this->settingsOption('url', 'slackLogWebhook', ['label' => 'Slack Webhook URL', 'help' => 'If using Discord make sure to end the URL with /slack']), $this->settingsOption('input', 'slackLogWebHookChannel', ['label' => 'Slack Channel for Webhook', 'help' => 'Channel ID for webhook - Not needed for Discord']), $this->settingsOption('blank'), $this->settingsOption('test', 'slack-logs', ['label' => 'Test Slack', 'text' => 'Test Slack', 'help' => 'Test only sends a warning message so make sure Slack Log Level is Warning when testing']), ], 'Cron' => [ $this->settingsOption('cron-file'), $this->settingsOption('blank'), $this->settingsOption('enable', 'autoUpdateCronEnabled', ['label' => 'Auto-Update Organizr']), $this->settingsOption('cron', 'autoUpdateCronSchedule'), $this->settingsOption('enable', 'autoBackupCronEnabled', ['label' => 'Auto-Backup Organizr']), $this->settingsOption('cron', 'autoBackupCronSchedule'), $this->settingsOption('number', 'keepBackupsCountCron', ['label' => '# Backups Keep', 'help' => 'Number of backups to keep', 'attr' => 'min="1"']), $this->settingsOption('folder', 'backupLocation', ['label' => 'Backup Save Path', 'help' => 'Folder path to save Organizr Backups - Please test before saving', 'value' => $this->getOrganizrBackupLocation()]), $this->settingsOption('blank'), ], 'Login' => [ $this->settingsOption('password', 'registrationPassword', ['label' => 'Registration Password', 'help' => 'Sets the password for the Registration form on the login screen']), $this->settingsOption('switch', 'hideRegistration', ['label' => 'Hide Registration', 'help' => 'Enable this to hide the Registration button on the login screen']), $this->settingsOption('number', 'rememberMeDays', ['label' => 'Remember Me Length', 'help' => 'Number of days cookies and tokens will be valid for', 'attr' => 'min="1"']), $this->settingsOption('switch', 'rememberMe', ['label' => 'Remember Me', 'help' => 'Default status of Remember Me button on login screen']), $this->settingsOption('multiple-url', 'localIPList', ['label' => 'Override Local IP or Subnet', 'help' => 'IPv4 only at the moment - This will set your login as local if your IP falls within the From and To']), $this->settingsOption('input', 'wanDomain', ['label' => 'WAN Domain', 'placeholder' => 'only domain and tld - i.e. domain.com', 'help' => 'Enter domain if you wish to be forwarded to a local address - Local Address filled out on next item']), $this->settingsOption('url', 'localAddress', ['label' => 'Local Address', 'placeholder' => 'http://home.local', 'help' => 'Full local address of organizr install - i.e. http://home.local or http://192.168.0.100']), $this->settingsOption('switch', 'enableLocalAddressForward', ['label' => 'Enable Local Address Forward', 'help' => 'Enables the local address forward if on local address and accessed from WAN Domain']), $this->settingsOption('switch', 'disableRecoverPass', ['label' => 'Disable Recover Password', 'help' => 'Disables recover password area']), $this->settingsOption('input', 'customForgotPassText', ['label' => 'Custom Recover Password Text', 'help' => 'Text or HTML for recovery password section']), ], 'Auth Proxy' => [ $this->settingsOption('switch', 'authProxyEnabled', ['label' => 'Auth Proxy', 'help' => 'Enable option to set Auth Proxy Header Login']), $this->settingsOption('input', 'authProxyWhitelist', ['label' => 'Auth Proxy Whitelist', 'placeholder' => 'i.e. 10.0.0.0/24 or 10.0.0.20', 'help' => 'IPv4 only at the moment - This must be set to work, will accept subnet or IP address']), $this->settingsOption('input', 'authProxyHeaderName', ['label' => 'Auth Proxy Header Name', 'placeholder' => 'i.e. X-Forwarded-User', 'help' => 'Please choose a unique value for added security']), $this->settingsOption('input', 'authProxyHeaderNameEmail', ['label' => 'Auth Proxy Header Name for Email', 'placeholder' => 'i.e. X-Forwarded-Email', 'help' => 'Please choose a unique value for added security']), $this->settingsOption('switch', 'authProxyOverrideLogout', ['label' => 'Override Logout', 'help' => 'Enable option to set custom Logout URL for Auth Proxy']), $this->settingsOption('input', 'authProxyLogoutURL', ['label' => 'Logout URL', 'help' => 'Logout URL to redirect user for Auth Proxy']), ], 'Ping' => [ $this->settingsOption('auth', 'pingAuth'), $this->settingsOption('auth', 'pingAuthMessage', ['label' => 'Minimum Authentication for Message and Sound']), $this->settingsOption('select', 'pingOnlineSound', ['label' => 'Online Sound', 'options' => $this->getSounds()]), $this->settingsOption('select', 'pingOfflineSound', ['label' => 'Offline Sound', 'options' => $this->getSounds()]), $this->settingsOption('switch', 'pingMs', ['label' => 'Show Ping Time']), $this->settingsOption('switch', 'statusSounds', ['label' => 'Enable Notify Sounds', 'help' => 'Will play a sound if the server goes down and will play sound if comes back up.']), $this->settingsOption('auth', 'pingAuthMs', ['label' => 'Minimum Authentication for Time Display']), $this->settingsOption('refresh', 'adminPingRefresh', ['label' => 'Admin Refresh Seconds']), $this->settingsOption('refresh', 'otherPingRefresh', ['label' => 'Everyone Refresh Seconds']), ], 'Certificate' => [ $this->settingsOption('html', '', ['override' => 12, 'html' => '
Notice
By default, Organizr uses certificates from https://curl.se/docs/caextract.html
If you would like to use your own certificate, please upload it below. You will then need to enable each homepage item to use it.

Custom Certificate Status

' . $certificateStatus . '

Drop Certificate file here to upload
'] ) ], ]; if ($this->config['driver'] == 'sqlite3') { $database = [ 'Database' => [ $this->settingsOption('notice', '', ['notice' => 'danger', 'title' => 'Warning', 'body' => 'This feature is experimental - You may face unexpected database is locked errors in logs']), $this->settingsOption('html', '', ['label' => 'Journal Mode Status', 'html' => '

']), $this->settingsOption('button', '', ['label' => 'Set DELETE Mode (Default)', 'icon' => 'icon-notebook', 'text' => 'Set', 'attr' => 'onclick="setJournalMode(\'DELETE\')"']), $this->settingsOption('button', '', ['label' => 'Set WAL Mode', 'icon' => 'icon-notebook', 'text' => 'Set', 'attr' => 'onclick="setJournalMode(\'WAL\')"']), ] ]; $settings = array_merge($settings, $database); } ksort($settings); return $settings; } public function getSettingsSSO() { return [ 'FYI' => [ $this->settingsOption('html', '', ['override' => 12, 'html' => '
Please Read First
Using multiple SSO application will cause your Cookie Header item to increase. If you haven\'t increased it by now, please follow this guide


This is not the same as database authentication - i.e. Plex Authentication | Emby Authentication | FTP Authentication
Click Main on the sub-menu above.
' ] ), ], 'Plex' => [ $this->settingsOption('token', 'plexToken'), $this->settingsOption('button', '', ['label' => 'Get Plex Token', 'icon' => 'fa fa-ticket', 'text' => 'Retrieve', 'attr' => 'onclick="PlexOAuth(oAuthSuccess,oAuthError, null, \'#sso-form [name=plexToken]\')"']), $this->settingsOption('password-alt', 'plexID', ['label' => 'Plex Machine']), $this->settingsOption('button', '', ['label' => 'Get Plex Machine', 'icon' => 'fa fa-id-badge', 'text' => 'Retrieve', 'attr' => 'onclick="showPlexMachineForm(\'#sso-form [name=plexID]\')"']), $this->settingsOption('input', 'plexAdmin', ['label' => 'Plex Admin Username or Email']), $this->settingsOption('blank'), $this->settingsOption('html', 'Plex Note', ['html' => 'Please make sure both Token and Machine are filled in']), $this->settingsOption('enable', 'ssoPlex'), ], 'Tautulli' => [ $this->settingsOption('multiple-url', 'tautulliURL'), $this->settingsOption('auth', 'ssoTautulliAuth'), $this->settingsOption('enable', 'ssoTautulli'), ], 'Overseerr' => [ $this->settingsOption('url', 'overseerrURL'), $this->settingsOption('token', 'overseerrToken'), $this->settingsOption('username', 'overseerrFallbackUser', ['label' => 'Overseerr Fallback Email', 'help' => 'DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a "catch all" for when Organizr is unable to perform SSO. Organizr will request a User Token based off of this user credentials']), $this->settingsOption('password', 'overseerrFallbackPassword', ['label' => 'Overseerr Fallback Password']), $this->settingsOption('enable', 'ssoOverseerr'), ], 'Petio' => [ $this->settingsOption('url', 'petioURL'), $this->settingsOption('token', 'petioToken'), $this->settingsOption('username', 'petioFallbackUser', ['label' => 'Petio Fallback Email', 'help' => 'DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a "catch all" for when Organizr is unable to perform SSO. Organizr will request a User Token based off of this user credentials']), $this->settingsOption('password', 'petioFallbackPassword', ['label' => 'Petio Fallback Password']), $this->settingsOption('enable', 'ssoPetio'), ], 'Ombi' => [ $this->settingsOption('url', 'ombiURL'), $this->settingsOption('token', 'ombiToken'), $this->settingsOption('username', 'ombiFallbackUser', ['label' => 'Ombi Fallback Email', 'help' => 'DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a "catch all" for when Organizr is unable to perform SSO. Organizr will request a User Token based off of this user credentials']), $this->settingsOption('password', 'ombiFallbackPassword', ['label' => 'Ombi Fallback Password']), $this->settingsOption('enable', 'ssoOmbi'), ], 'Jellyfin' => [ $this->settingsOption('url', 'jellyfinURL', ['label' => 'Jellyfin API URL', 'help' => 'Please make sure to use the local address to the API']), $this->settingsOption('url', 'jellyfinSSOURL', ['label' => 'Jellyfin SSO URL', 'help' => 'Please make sure to use the same (sub)domain to access Jellyfin as Organizr\'s']), $this->settingsOption('enable', 'ssoJellyfin'), ], 'Komga' => [ $this->settingsOption('url', 'komgaURL'), $this->settingsOption('auth', 'ssoKomgaAuth'), $this->settingsOption('enable', 'ssoKomga'), $this->settingsOption('blank'), $this->settingsOption('username', 'komgaFallbackUser', ['label' => 'Komga Fallback Email', 'help' => 'DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a "catch all" for when Organizr is unable to perform SSO. Organizr will request a User Token based off of this user credentials']), $this->settingsOption('password', 'komgaFallbackPassword', ['label' => 'Komga Fallback Password']), $this->settingsOption('password', 'komgaSSOMasterPassword', ['label' => 'Komga Master Password', 'help' => 'Sets master password if using oAuth backend - This will set the password on the login form for logins using oAuth where no password is supplied.']), ], ]; } public function systemMenuLists() { $pluginsMenu = [ [ 'active' => false, 'api' => 'api/v2/page/settings_plugins_enabled', 'anchor' => 'settings-plugins-enabled-anchor', 'name' => 'Active', ], [ 'active' => false, 'api' => 'api/v2/page/settings_plugins_disabled', 'anchor' => 'settings-plugins-disabled-anchor', 'name' => 'Inactive', ], [ 'active' => false, 'api' => 'api/v2/page/settings_plugins_settings', 'anchor' => 'settings-plugins-settings-anchor', 'name' => 'Settings', ], [ 'active' => false, 'api' => false, 'anchor' => 'settings-plugins-marketplace-anchor', 'name' => 'Marketplace', 'onclick' => 'loadPluginMarketplace();' ], ]; $userManagementMenu = [ [ 'active' => false, 'api' => 'api/v2/page/settings_user_manage_users', 'anchor' => 'settings-user-manage-users-anchor', 'name' => 'Manage Users' ], [ 'active' => false, 'api' => 'api/v2/page/settings_user_manage_groups', 'anchor' => 'settings-user-manage-groups-anchor', 'name' => 'Manage Groups' ], [ 'active' => false, 'api' => false, 'anchor' => 'settings-user-import-users-anchor', 'name' => 'Import Users' ], ]; $customizeMenu = [ [ 'active' => false, 'api' => 'api/v2/page/settings_customize_appearance', 'anchor' => 'settings-customize-appearance-anchor', 'name' => 'Appearance', ], [ 'active' => false, 'api' => 'api/v2/page/settings_customize_settings', 'anchor' => 'settings-customize-settings-anchor', 'name' => 'Marketplace Settings', ], [ 'active' => false, 'api' => false, 'anchor' => 'settings-customize-marketplace-anchor', 'name' => 'Marketplace', 'onclick' => 'loadThemeMarketplace();' ], ]; $tabEditorMenu = [ [ 'active' => false, 'api' => 'api/v2/page/settings_tab_editor_tabs', 'anchor' => 'settings-tab-editor-tabs-anchor', 'name' => 'Tabs' ], [ 'active' => false, 'api' => 'api/v2/page/settings_tab_editor_categories', 'anchor' => 'settings-tab-editor-categories-anchor', 'name' => 'Categories' ], [ 'active' => false, 'api' => 'api/v2/page/settings_tab_editor_homepage', 'anchor' => 'settings-tab-editor-homepage-anchor', 'name' => 'Homepage Items' ], [ 'active' => false, 'api' => 'api/v2/page/settings_tab_editor_homepage_order', 'anchor' => 'settings-tab-editor-homepage-order-anchor', 'name' => 'Homepage Order' ], ]; $systemSettingsMenu = [ [ 'active' => true, 'api' => false, 'anchor' => 'settings-settings-about-anchor', 'name' => 'About' ], [ 'active' => false, 'api' => 'api/v2/page/settings_settings_main', 'anchor' => 'settings-settings-main-anchor', 'name' => 'Main' ], [ 'active' => false, 'api' => 'api/v2/page/settings_settings_sso', 'anchor' => 'settings-settings-sso-anchor', 'name' => 'SSO' ], [ 'active' => false, 'api' => 'api/v2/page/settings_settings_logs', 'anchor' => 'settings-settings-logs-anchor', 'name' => 'Logs' ], [ 'active' => false, 'api' => false, 'anchor' => 'settings-settings-updates-anchor', 'name' => 'Updates' ], [ 'active' => false, 'api' => 'api/v2/page/settings_settings_backup', 'anchor' => 'settings-settings-backup-anchor', 'name' => 'Backup' ], [ 'active' => false, 'api' => false, 'anchor' => 'settings-settings-donate-anchor', 'name' => 'Donate' ], ]; $systemMenus['system_settings'] = $this->buildSettingsMenus($systemSettingsMenu, 'System Settings'); $systemMenus['tab_editor'] = $this->buildSettingsMenus($tabEditorMenu, 'Tab Editor'); $systemMenus['customize'] = $this->buildSettingsMenus($customizeMenu, 'Customize'); $systemMenus['user_management'] = $this->buildSettingsMenus($userManagementMenu, 'User Management'); $systemMenus['plugins'] = $this->buildSettingsMenus($pluginsMenu, 'Plugins'); return $systemMenus; } public function updateConfigMultiple($array) { return (bool)$this->updateConfig($array); } public function updateConfigItems($array) { if (!count($array)) { $this->setAPIResponse('error', 'No data submitted', 409); return false; } $newItems = []; $updatedItems = []; foreach ($array as $k => $v) { $v = $v ?? ''; switch ($v) { case 'true': $v = (bool)true; break; case 'false': $v = (bool)false; break; } // Hash if ((stripos($k, 'password') !== false)) { if (!$this->isEncrypted($v)) { if ($v !== '') { $v = $this->encrypt($v); } } } switch ($k) { case 'logLocation': case 'dbLocation': if (!empty($v)) { $v = $this->cleanDirectory($v); } break; default: break; } if (strtolower($k) !== 'formkey') { if ($this->config[$k] !== $v) { $updatedItems[$k] = $v; } $newItems[$k] = $v; $this->config[$k] = $v; } } $this->setAPIResponse('success', 'Config items updated', 200); $this->setLoggerChannel('Config')->notice('Config items updated', ['items' => array_keys($updatedItems)]); return (bool)$this->updateConfig($newItems); } public function updateConfigItem($array) { $array['value'] = $array['value'] ?? ''; switch ($array['value']) { case 'true': $array['value'] = (bool)true; break; case 'false': $array['value'] = (bool)false; break; } // Hash if ($array['type'] == 'password') { $array['value'] = $this->encrypt($array['value']); } $newItem = array( $array['name'] => $array['value'] ); $this->config[$array['name']] = $array['value']; return (bool)$this->updateConfig($newItem); } public function ignoreNewsId($id) { if (!$id) { $this->setAPIResponse('error', 'News id was not supplied', 409); return false; } $id = array(intval($id)); $newsIds = $this->config['ignoredNewsIds']; $newsIds = array_merge($newsIds, $id); $newsIds = array_unique($newsIds); $this->updateConfig(['ignoredNewsIds' => $newsIds]); $this->setAPIResponse('success', 'News id is now ignored', 200, null); } public function getNewsIds() { $newsIds = $this->config['ignoredNewsIds']; $this->setAPIResponse('success', null, 200, $newsIds); return $newsIds; } public function testWizardPath($array) { if ($this->hasDB()) { $this->setAPIResponse('error', 'Endpoint disabled as database already exists', 401); return false; } $path = $array['path'] ?? null; if (file_exists($path)) { if (is_writable($path)) { $this->setAPIResponse('success', 'Path exists and is writable', 200); return true; } else { $this->setAPIResponse('error', 'Path exists but is not writable', 403); return false; } } else { if (mkdir($path, 0760, true)) { $this->setAPIResponse('success', 'Path is writable - Creating now', 200); return true; } else { $this->setAPIResponse('error', 'Failed making directory - Check permissions', 403); return false; } } } public function formatDatabaseDriver($driver) { $driver = strtolower($driver); switch ($driver) { case 'sqlite': case 'sqlite3': return 'sqlite3'; case 'mysql': case 'mysqli': return 'mysqli'; case 'postgre': case 'postgres': case 'postgresql': return 'postgre'; default: return $driver; } } public function wizardConfig($array) { $array['driver'] = $array['driver'] ?? 'sqlite3'; $driver = $this->formatDatabaseDriver($array['driver']); $dbName = $array['dbName'] ?? null; $path = $array['dbPath'] ?? $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . $this->random_ascii_string(10) . DIRECTORY_SEPARATOR; $license = $array['license'] ?? null; $hashKey = $array['hashKey'] ?? null; $api = $array['api'] ?? null; $registrationPassword = $array['registrationPassword'] ?? null; $username = $array['username'] ?? null; $password = $array['password'] ?? null; $email = $array['email'] ?? null; $dbHost = $array['dbHost'] ?? null; $dbUsername = $array['dbUsername'] ?? null; $dbPassword = $array['dbPassword'] ?? null; $validation = [ 'dbPath' => $path, 'dbName' => $dbName, 'license' => $license, 'hashKey' => $hashKey, 'api' => $api, 'registrationPassword' => $registrationPassword, 'username' => $username, 'password' => $password, 'email' => $email, 'driver' => $driver ]; $dbName = $this->dbExtension($dbName); if ($driver == 'mysqli' || $driver == 'postgre') { $dbName = $this->removeDbExtension($dbName); $validation = array_merge($validation, [ 'dbHost' => $dbHost, 'dbUsername' => $dbUsername, 'dbPassword' => $dbPassword, ]); } foreach ($validation as $k => $v) { if ($v == null) { $this->setAPIResponse('error', '[' . $k . '] cannot be empty', 422); return false; } } if ($path) { $path = $this->cleanDirectory($path); if (file_exists($path)) { if (!is_writable($path)) { $this->setAPIResponse('error', '[' . $path . '] is not writable', 422); return false; } } else { if (!mkdir($path, 0760, true)) { $this->setAPIResponse('error', '[' . $path . '] is not writable', 422); return false; } } } $configVersion = $this->version; $configArray = array( 'dbLocation' => $path, 'driver' => $driver, 'dbName' => $dbName, 'license' => $license, 'organizrHash' => $hashKey, 'organizrAPI' => $api, 'registrationPassword' => $registrationPassword, 'uuid' => $this->gen_uuid() ); if ($driver == 'mysqli' || $driver == 'postgre') { $configArray = array_merge($configArray, [ 'dbHost' => $dbHost, 'dbUsername' => $dbUsername, 'dbPassword' => $this->encrypt($dbPassword, $hashKey), ]); } //Test Database Connection before saving config $testDatabaseConnection = $this->testDatabaseConnection($array); if (!$testDatabaseConnection) { // setResponse already defined return false; } // Create Config if ($this->createConfig($configArray)) { $this->config = $this->config(); $this->refreshCookieName(); $this->connectDB(); // Call DB Create if (!$this->createNewDB($dbName, false)) { $this->setAPIResponse('error', 'error creating database using driver: ' . $driver, 500); return false; } if ($this->createDB($path)) { // Add in first user if ($this->createFirstAdmin($username, $password, $email)) { if ($this->createToken($username, $email, 1)) { return true; } else { $this->setAPIResponse('error', 'error creating token', 500); } } else { $this->setAPIResponse('error', 'error creating admin', 500); } } else { $this->setAPIResponse('error', 'error creating database', 500); } } else { $this->setAPIResponse('error', 'error creating config', 500); } return false; } public function testDatabaseConnection($array) { $driver = $array['driver'] ?? 'sqlite3'; $driver = $this->formatDatabaseDriver($driver); $dbName = $array['dbName'] ?? null; $dbHost = $array['dbHost'] ?? null; $dbUsername = $array['dbUsername'] ?? null; $dbPassword = $array['dbPassword'] ?? null; $path = $array['dbPath'] ?? $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . $this->random_ascii_string(10) . DIRECTORY_SEPARATOR; switch ($driver) { case 'mysqli': if (!extension_loaded('mysqli')) { $this->setResponse(500, 'PHP Extension `mysqli` is not loaded'); return false; } $config = [ 'driver' => 'mysqli', 'host' => $dbHost, 'username' => $dbUsername, 'password' => $dbPassword, 'options' => [ MYSQLI_OPT_CONNECT_TIMEOUT => 60, ], 'flags' => MYSQLI_CLIENT_COMPRESS, ]; $validation = [ 'dbHost' => $dbHost, 'dbUsername' => $dbUsername, 'dbPassword' => $dbPassword, ]; break; case 'postgre': // DISABLE FOR NOW $this->setResponse(409, 'Database connection test not available for driver: ' . $driver); return false; $config = [ //host, hostaddr, port, dbname, user, password, connect_timeout, options, sslmode, service => see PostgreSQL API 'driver' => 'postgre', 'username' => $dbUsername, 'password' => $dbPassword, 'persistent' => true, ]; $host = $this->qualifyURL($dbHost, true); if ($host['port']) { $config = array_merge($config, ['port' => ltrim($host['port'], ':')]); } if (($host['host'])) { $config = array_merge($config, ['host' => $host['host']]); } if (!$host['host'] && $host['path']) { $config = array_merge($config, ['host' => $host['path']]); } $validation = [ 'dbHost' => $dbHost, 'dbUsername' => $dbUsername, 'dbPassword' => $dbPassword, ]; break; case 'sqlite3': $path = $this->cleanDirectory($path); if (file_exists($path)) { if (!is_writable($path)) { $this->setAPIResponse('error', '[' . $path . '] is not writable', 422); return false; } } else { if (is_writable(dirname($path, 1))) { if (!mkdir($path, 0760, true)) { $this->setAPIResponse('error', '[' . $path . '] is not writable', 422); return false; } } else { $this->setAPIResponse('error', '[' . $path . '] is not writable', 422); return false; } } return true; default: $this->setResponse(409, 'Database connection test not available for driver: ' . $driver); return false; } foreach ($validation as $k => $v) { if ($v == null) { $this->setAPIResponse('error', '[' . $k . '] cannot be empty', 422); return false; } } try { $connection = new Connection($config); $testConnection = $connection->isConnected(); if ($testConnection) { $this->setResponse(200, 'Database connection successful'); return true; } else { $this->setResponse(409, 'Database connection unsuccessful'); return false; } } catch (Dibi\Exception $e) { $this->setResponse(500, $e->getMessage()); return false; } } public function createNewDB($dbName, $migration = false) { if ($this->config['driver'] == 'mysqli') { $config = [ 'driver' => 'mysqli', 'host' => $this->config['dbHost'], 'username' => $this->config['dbUsername'], 'password' => $this->decrypt($this->config['dbPassword']), 'options' => [ MYSQLI_OPT_CONNECT_TIMEOUT => 60, ], 'flags' => MYSQLI_CLIENT_COMPRESS, ]; if ($migration) { try { $this->otherDb = new Connection($config); } catch (Dibi\Exception $e) { $this->otherDb = null; } } else { try { $this->db = new Connection($config); } catch (Dibi\Exception $e) { $this->db = null; } } if ($dbName == 'tempMigration') { $response = [ array( 'function' => 'query', 'query' => [ 'DROP DATABASE IF EXISTS tempMigration' ] ), ]; $drop = $this->processQueries($response, $migration); } $response = [ array( 'function' => 'query', 'query' => ['CREATE DATABASE IF NOT EXISTS %n', $dbName ] ) ]; $results = $this->processQueries($response, $migration); if ($results) { if ($migration) { $this->connectOtherDB(); } else { $this->connectDB(); } return true; } else { return false; } } elseif ($this->config['driver'] == 'postgre') { $config = [ 'driver' => 'postgre', 'username' => $this->config['dbUsername'], 'password' => $this->decrypt($this->config['dbPassword']), 'persistent' => true, ]; $host = $this->qualifyURL($this->config['dbHost'], true); if ($host['port']) { $config = array_merge($config, ['port' => ltrim($host['port'], ':')]); } if ($host['host']) { $config = array_merge($config, ['host' => $host['host']]); } if (!$host['host'] && $host['path']) { $config = array_merge($config, ['host' => $host['path']]); } try { $this->db = new Connection($config); } catch (Dibi\Exception $e) { $this->db = null; } $response = [ array( 'function' => 'query', 'query' => ['CREATE DATABASE %n', $dbName ] ) ]; $results = $this->processQueries($response, $migration); if ($results) { $this->connectDB(); return true; } else { return false; } } else { return true; } } public function getDefaultTablesFormatted() { $list = []; $tables = $this->defaultTables(); foreach ($tables as $table) { $string = trim($table['query']); preg_match('/CREATE TABLE `(.*?)`/', $string, $output_array); if (count($output_array) > 1) { $list[] = $output_array[1]; } } return $list; } public function defaultTables() { return [ array( 'function' => 'query', 'query' => ' CREATE TABLE `users` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `username` TEXT UNIQUE, `password` TEXT, `email` TEXT, `plex_token` TEXT, `group` TEXT, `group_id` INTEGER, `locked` INTEGER, `image` TEXT, `register_date` DATETIME, `auth_service` TEXT DEFAULT \'internal\' ); ' ), array( 'function' => 'query', 'query' => 'CREATE TABLE `chatroom` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `username` TEXT, `gravatar` TEXT, `uid` TEXT, `date` DATETIME, `ip` TEXT, `message` TEXT );' ), array( 'function' => 'query', 'query' => 'CREATE TABLE `tokens` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `token` TEXT UNIQUE, `user_id` INTEGER, `browser` TEXT, `ip` TEXT, `created` DATETIME, `expires` DATETIME );' ), array( 'function' => 'query', 'query' => 'CREATE TABLE `groups` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `group` TEXT UNIQUE, `group_id` INTEGER, `image` TEXT, `default` INTEGER );' ), array( 'function' => 'query', 'query' => 'CREATE TABLE `categories` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `order` INTEGER, `category` TEXT UNIQUE, `category_id` INTEGER, `image` TEXT, `default` INTEGER );' ), array( 'function' => 'query', 'query' => 'CREATE TABLE `tabs` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `order` INTEGER, `category_id` INTEGER, `name` TEXT, `url` TEXT, `url_local` TEXT, `default` INTEGER, `enabled` INTEGER, `group_id` INTEGER, `group_id_max` INTEGER DEFAULT \'0\', `add_to_admin` INTEGER DEFAULT \'0\', `image` TEXT, `type` INTEGER, `splash` INTEGER, `ping` INTEGER, `ping_url` TEXT, `timeout` INTEGER, `timeout_ms` INTEGER, `preload` INTEGER );' ), array( 'function' => 'query', 'query' => 'CREATE TABLE `options` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `name` TEXT UNIQUE, `value` TEXT );' ), array( 'function' => 'query', 'query' => 'CREATE TABLE `invites` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `code` TEXT UNIQUE, `date` DATETIME, `email` TEXT, `username` TEXT, `dateused` TIMESTAMP, `usedby` TEXT, `ip` TEXT, `valid` TEXT, `type` TEXT, `invitedby` TEXT );' ), array( 'function' => 'query', 'query' => 'CREATE TABLE `BOOKMARK-categories` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `order` INTEGER, `category` TEXT UNIQUE, `category_id` INTEGER, `default` INTEGER );' ), array( 'function' => 'query', 'query' => 'CREATE TABLE `BOOKMARK-tabs` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, `order` INTEGER, `category_id` INTEGER, `name` TEXT, `url` TEXT, `enabled` INTEGER, `group_id` INTEGER, `image` TEXT, `background_color` TEXT, `text_color` TEXT );' ) ]; } public function createDB($path, $migration = false) { if (!file_exists($path)) { mkdir($path, 0777, true); } $tables = $this->defaultTables(); return $this->processQueries($tables, $migration); } public function createFirstAdmin($username, $password, $email) { $userInfo = [ 'username' => $username, 'password' => password_hash($password, PASSWORD_BCRYPT), 'email' => $email, 'group' => 'Admin', 'group_id' => 0, 'image' => $this->gravatar($email), 'register_date' => gmdate('Y-m-d H:i:s'), ]; $groupInfo0 = [ 'group' => 'Admin', 'group_id' => 0, 'default' => 0, 'image' => 'plugins/images/groups/admin.png', ]; $groupInfo1 = [ 'group' => 'Co-Admin', 'group_id' => 1, 'default' => 0, 'image' => 'plugins/images/groups/coadmin.png', ]; $groupInfo2 = [ 'group' => 'Super User', 'group_id' => 2, 'default' => 0, 'image' => 'plugins/images/groups/superuser.png', ]; $groupInfo3 = [ 'group' => 'Power User', 'group_id' => 3, 'default' => 0, 'image' => 'plugins/images/groups/poweruser.png', ]; $groupInfo4 = [ 'group' => 'User', 'group_id' => 4, 'default' => 1, 'image' => 'plugins/images/groups/user.png', ]; $groupInfoGuest = [ 'group' => 'Guest', 'group_id' => 999, 'default' => 0, 'image' => 'plugins/images/groups/guest.png', ]; $settingsInfo = [ 'order' => 1, 'category_id' => 0, 'name' => 'Settings', 'url' => 'api/v2/page/settings', 'default' => 0, 'enabled' => 1, 'group_id' => 1, 'image' => 'fontawesome::cog', 'type' => 0 ]; $homepageInfo = [ 'order' => 2, 'category_id' => 0, 'name' => 'Homepage', 'url' => 'api/v2/page/homepage', 'default' => 0, 'enabled' => 0, 'group_id' => 4, 'image' => 'fontawesome::home', 'type' => 0 ]; $unsortedInfo = [ 'order' => 1, 'category' => 'Unsorted', 'category_id' => 0, 'image' => 'fontawesome::question', 'default' => 1 ]; $response = [ array( 'function' => 'query', 'query' => array( 'INSERT INTO [users]', $userInfo ) ), array( 'function' => 'query', 'query' => array( 'INSERT INTO [groups]', $groupInfo0 ) ), array( 'function' => 'query', 'query' => array( 'INSERT INTO [groups]', $groupInfo1 ) ), array( 'function' => 'query', 'query' => array( 'INSERT INTO [groups]', $groupInfo2 ) ), array( 'function' => 'query', 'query' => array( 'INSERT INTO [groups]', $groupInfo3 ) ), array( 'function' => 'query', 'query' => array( 'INSERT INTO [groups]', $groupInfo4 ) ), array( 'function' => 'query', 'query' => array( 'INSERT INTO [groups]', $groupInfoGuest ) ), array( 'function' => 'query', 'query' => array( 'INSERT INTO [tabs]', $settingsInfo ) ), array( 'function' => 'query', 'query' => array( 'INSERT INTO [tabs]', $homepageInfo ) ), array( 'function' => 'query', 'query' => array( 'INSERT INTO [categories]', $unsortedInfo ) ), ]; return $this->processQueries($response); } public function getUserByUsernameAndEmail($username, $email) { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM users WHERE username = ? COLLATE NOCASE OR email = ? COLLATE NOCASE', [$username], [$email] ) ), ]; return $this->processQueries($response); } public function createToken($username, $email, $days = 1) { $this->setLoggerChannel('Authentication', $username); $this->logger->debug('Starting token creation function'); $days = ($days > 365) ? 365 : $days; //Quick get user ID $result = $this->getUserByUsernameAndEmail($username, $email); $config = $this->configToken(); assert($config instanceof Lcobucci\JWT\Configuration); $now = new DateTimeImmutable(); $token = $config->builder() // Configures the issuer (iss claim) ->issuedBy('Organizr') // Configures the audience (aud claim) ->permittedFor('Organizr') // Configures the id (jti claim) ->identifiedBy('4f1g23a12aa') // Configures the time that the token was issue (iat claim) ->issuedAt($now) // Configures the time that the token can be used (nbf claim) ->canOnlyBeUsedAfter($now) // Configures the expiration time of the token (exp claim) ->expiresAt($now->modify('+' . $days . ' days')) // Configures a new claim, called "uid" ->withClaim('name', $result['username'])// Configures a new claim, called "name" ->withClaim('group', $result['group'])// Configures a new claim, called "group" ->withClaim('groupID', $result['group_id'])// Configures a new claim, called "groupID" ->withClaim('email', $result['email'])// Configures a new claim, called "email" ->withClaim('image', $result['image'])// Configures a new claim, called "image" ->withClaim('userID', $result['id'])// Configures a new claim, called "image" // Configures a new header, called "foo" //->withHeader('foo', 'bar') // Builds a new token ->getToken($config->signer(), $config->signingKey()); //$token->headers(); // Retrieves the token headers //$token->claims(); // Retrieves the token claims $this->coookie('set', $this->cookieName, $token->toString(), $days); // Add token to DB $addToken = [ 'token' => $token->toString(), 'user_id' => $result['id'], 'created' => gmdate('Y-m-d H:i:s'), 'browser' => $_SERVER ['HTTP_USER_AGENT'] ?? null, 'ip' => $this->userIP(), 'expires' => gmdate('Y-m-d H:i:s', time() + (86400 * $days)) ]; $response = [ [ 'function' => 'query', 'query' => [ 'INSERT INTO [tokens]', $addToken ], 'key' => 'insert' ], [ 'function' => 'fetchAll', 'query' => [ 'SELECT * FROM `tokens` WHERE user_id = ? ORDER BY `id` DESC LIMIT 100', [$result['id']] ], 'key' => 'tokens' ], ]; $query = $this->processQueries($response); if ($token) { $this->logger->debug('Token has been created'); $browserCount = array_column($query['tokens'], 'browser'); $browserCount = array_count_values($browserCount); if (isset($browserCount[$_SERVER ['HTTP_USER_AGENT']])) { if ($browserCount[$_SERVER ['HTTP_USER_AGENT']] <= 1) { if ($this->config['PHPMAILER-enabled']) { $PhpMailer = new PhpMailer(); $emailTemplate = array( 'type' => 'device', 'body' => '

Hey there {user}!

We noticed a login attempt to your account and want to make sure it\'s you.
If this was you, please ignore this email.

If this wasn\'t you, please change your password and revoke all tokens.

Details:
IP: ' . $this->userIP() . '
Browser: ' . $_SERVER ['HTTP_USER_AGENT'] . '
', 'subject' => 'We noticed a login attempt to your account on a new device.', 'user' => $result['username'], 'password' => null, 'inviteCode' => null, ); $emailTemplate = $PhpMailer->_phpMailerPluginEmailTemplate($emailTemplate); $sendEmail = array( 'to' => $result['email'], 'subject' => $emailTemplate['subject'], 'body' => $PhpMailer->_phpMailerPluginBuildEmail($emailTemplate), ); $response = $PhpMailer->_phpMailerPluginSendEmail($sendEmail); if ($response == true) { $this->logger->debug('Sent new device email'); } else { $this->logger->debug('Could not send new device email'); } } else { $this->logger->debug('Email not setup - cannot send new device email'); } } } else { $this->logger->debug('Could not find token in database'); } } else { $this->logger->warning('Token creation error'); } $this->logger->debug('Token creation function has finished'); return $token->toString(); } public function login($array) { // Grab username, Password & other optional items from api call $username = $array['username'] ?? null; $password = $array['password'] ?? null; $oAuth = $array['oAuth'] ?? null; $oAuthType = $array['oAuthType'] ?? null; $remember = $array['remember'] ?? null; $tfaCode = $array['tfaCode'] ?? null; $loginAttempts = $array['loginAttempts'] ?? null; $output = $array['output'] ?? null; $username = (strpos($this->config['authBackend'], 'emby') !== false) ? $username : strtolower($username); $days = (isset($remember)) ? $this->config['rememberMeDays'] : 1; // Set logger channel $this->setLoggerChannel('Authentication', $username); $this->logger->debug('Starting login function'); // Set other variables $function = 'plugin_auth_' . $this->config['authBackend']; $authSuccess = false; $authProxy = false; $addEmailToAuthProxy = true; $bypassTFA = false; // Check Login attempts and kill if over limit if ($loginAttempts > $this->config['loginAttempts'] || isset($_COOKIE['lockout'])) { $this->coookieSeconds('set', 'lockout', $this->config['loginLockout'], $this->config['loginLockout']); $this->logger->warning('User is locked out'); $this->setAPIResponse('error', 'User is locked out', 403); return false; } // Check if Auth Proxy is enabled if ($this->config['authProxyEnabled'] && ($this->config['authProxyHeaderName'] !== '' || $this->config['authProxyHeaderNameEmail'] !== '') && $this->config['authProxyWhitelist'] !== '') { if (isset($this->getallheadersi()[strtolower($this->config['authProxyHeaderName'])]) || isset($this->getallheadersi()[strtolower($this->config['authProxyHeaderNameEmail'])])) { $usernameHeader = $this->getallheadersi()[strtolower($this->config['authProxyHeaderName'])] ?? null; $emailHeader = $this->getallheadersi()[strtolower($this->config['authProxyHeaderNameEmail'])] ?? null; $headerForLogin = $usernameHeader ?: ($emailHeader ?: null); $this->setLoggerChannel('Authentication', $headerForLogin); $this->logger->debug('Starting Auth Proxy verification'); $whitelistRange = $this->analyzeIP($this->config['authProxyWhitelist']); $authProxy = $this->authProxyRangeCheck($whitelistRange['from'], $whitelistRange['to']); $username = ($authProxy) ? $headerForLogin : $username; $password = ($password == null) ? $this->random_ascii_string(10) : $password; $addEmailToAuthProxy = ($authProxy && $emailHeader) ? ['email' => $emailHeader] : true; if ($authProxy) { $this->logger->info('User has been verified using Auth Proxy'); $bypassTFA = true; } else { $this->logger->warning('User has failed verification using Auth Proxy'); } } } // Check if Login method was an oAuth login if (!$oAuth) { $result = $this->getUserByUsernameAndEmail($username, $username); $result['password'] = $result['password'] ?? ''; // Switch AuthType - internal - external - both switch ($this->config['authType']) { case 'external': if (method_exists($this, $function)) { $authSuccess = $this->$function($username, $password); } break; /** @noinspection PhpMissingBreakStatementInspection */ case 'both': if (method_exists($this, $function)) { $authSuccess = $this->$function($username, $password); } // no break default: // Internal if (!$authSuccess) { // perform the internal authentication step if (password_verify($password, $result['password'])) { $this->logger->debug('User password has been verified'); $authSuccess = true; } } } $authSuccess = ($authProxy) ? $addEmailToAuthProxy : $authSuccess; } else { // Has oAuth Token! switch ($oAuthType) { case 'plex': if ($this->config['plexoAuth']) { $this->logger->debug('Starting Plex oAuth verification'); $tokenInfo = $this->checkPlexToken($oAuth); if ($tokenInfo) { $authSuccess = [ 'username' => $tokenInfo['user']['username'], 'email' => $tokenInfo['user']['email'], 'image' => $tokenInfo['user']['thumb'], 'token' => $tokenInfo['user']['authToken'], 'oauth' => 'plex' ]; $this->logger->debug('User\'s Plex Token has been verified'); $this->coookie('set', 'oAuth', 'true', $this->config['rememberMeDays']); $authSuccess = ((!empty($this->config['plexAdmin']) && strtolower($this->config['plexAdmin']) == strtolower($tokenInfo['user']['username'])) || (!empty($this->config['plexAdmin']) && strtolower($this->config['plexAdmin']) == strtolower($tokenInfo['user']['email'])) || $this->checkPlexUser($tokenInfo['user']['username'])) ? $authSuccess : false; } else { $this->logger->warning('User\'s Plex Token has failed verification'); } } else { $this->logger->debug('Plex oAuth is not setup'); $this->setAPIResponse('error', 'Plex oAuth is not setup', 422); return false; } break; default: return ($output) ? 'No oAuthType defined' : 'error'; } $result = ($authSuccess) ? $this->getUserByUsernameAndEmail($authSuccess['username'], $authSuccess['email']) : ''; } if ($authSuccess) { // Make sure user exists in database $userExists = false; $passwordMatches = $oAuth || $authProxy; $token = (is_array($authSuccess) && isset($authSuccess['token']) ? $authSuccess['token'] : ''); if (isset($result['username'])) { $userExists = true; $username = $result['username']; if ($passwordMatches == false) { $passwordMatches = password_verify($password, $result['password']); } } if ($userExists) { //does org password need to be updated if (!$passwordMatches) { $this->updateUserPassword($password, $result['id']); $this->setLoggerChannel('Authentication', $username); $this->logger->info('User Password updated from backend'); } if ($token !== '') { if ($token !== $result['plex_token']) { $this->updateUserPlexToken($token, $result['id']); $this->setLoggerChannel('Authentication', $username); $this->logger->info('User Plex Token updated from backend'); } } // 2FA might go here if ($result['auth_service'] !== 'internal' && strpos($result['auth_service'], '::') !== false) { $tfaProceed = true; // Add check for local or not if ($this->config['ignoreTFALocal'] !== false) { $tfaProceed = !$this->isLocal(); } // Is Plex Oauth? if ($this->config['ignoreTFAIfPlexOAuth'] !== false) { if (isset($authSuccess['oauth'])) { if ($authSuccess['oauth'] == 'plex') { $tfaProceed = false; } } } if ($bypassTFA) { $tfaProceed = false; $this->setLoggerChannel('Authentication', $username); $this->logger->info('Bypassing 2FA'); } if ($tfaProceed) { $this->setLoggerChannel('Authentication', $username); $this->logger->debug('Starting 2FA verification'); $TFA = explode('::', $result['auth_service']); // Is code with login info? if ($tfaCode == '') { $this->logger->debug('Sending 2FA response to login UI'); $this->setAPIResponse('warning', '2FA Code Needed', 422); return false; } else { if (!$this->verify2FA($TFA[1], $tfaCode, $TFA[0])) { $this->logger->warning('Incorrect 2FA'); $this->setAPIResponse('error', 'Wrong 2FA', 422); return false; } else { $this->logger->info('2FA verification passed'); } } } } // End 2FA // authentication passed - 1) mark active and update token $createToken = $this->createToken($result['username'], $result['email'], $days); if ($createToken) { $this->logger->info('User has logged in'); $ssoUserObject = ($token !== '') ? $authSuccess : $result; $this->ssoCheck($ssoUserObject, $password, $token); //need to work on this return ($output) ? array('name' => $this->cookieName, 'token' => (string)$createToken) : true; } else { $this->setAPIResponse('error', 'Token creation error', 500); return false; } } else { // Create User $this->setLoggerChannel('Authentication', (is_array($authSuccess) && isset($authSuccess['username']) ? $authSuccess['username'] : $username)); $this->logger->debug('Starting Registration function'); return $this->authRegister((is_array($authSuccess) && isset($authSuccess['username']) ? $authSuccess['username'] : $username), $password, (is_array($authSuccess) && isset($authSuccess['email']) ? $authSuccess['email'] : ''), $token); } } else { // authentication failed $this->setLoggerChannel('Authentication', $username); $this->logger->warning('Wrong Password'); if ($loginAttempts >= $this->config['loginAttempts']) { $this->logger->warning('User exceeded maximum login attempts'); $this->coookieSeconds('set', 'lockout', $this->config['loginLockout'], $this->config['loginLockout']); $this->setAPIResponse('error', 'User is locked out', 403); return false; } else { $this->logger->debug('User has not exceeded maximum login attempts'); $this->setAPIResponse('error', 'User credentials incorrect', 401); return false; } } } public function logout() { $this->setLoggerChannel('Authentication'); $this->logger->debug('Starting log out process'); $this->logger->info('User has logged out'); $this->coookie('delete', $this->cookieName); $this->coookie('delete', 'mpt'); $this->coookie('delete', 'Auth'); $this->coookie('delete', 'oAuth'); $this->coookie('delete', 'connect.sid'); $this->coookie('delete', 'petio_jwt'); $this->clearTautulliTokens(); $this->clearJellyfinTokens(); $this->revokeTokenCurrentUser($this->user['token']); $this->clearKomgaToken(); $this->refreshDeviceUUID(); $this->logger->debug('Log out process has finished'); $this->user = null; return true; } public function recover($array) { $email = $array['email'] ?? null; if (!$email) { $this->setAPIResponse('error', 'Email was not supplied', 422); return false; } $newPassword = $this->randString(10); $isUser = $this->getUserByEmail($email); if ($isUser) { $this->updateUserPassword($newPassword, $isUser['id']); $this->setAPIResponse('success', 'User password has been reset', 200); $this->setLoggerChannel('User Management'); $this->logger->info('User Management Function - User: ' . $isUser['username'] . '\'s password was reset'); if ($this->config['PHPMAILER-enabled']) { $PhpMailer = new PhpMailer(); $emailTemplate = array( 'type' => 'reset', 'body' => $this->config['PHPMAILER-emailTemplateReset'], 'subject' => $this->config['PHPMAILER-emailTemplateResetSubject'], 'user' => $isUser['username'], 'password' => $newPassword, 'inviteCode' => null, ); $emailTemplate = $PhpMailer->_phpMailerPluginEmailTemplate($emailTemplate); $sendEmail = array( 'to' => $email, 'user' => $isUser['username'], 'subject' => $emailTemplate['subject'], 'body' => $PhpMailer->_phpMailerPluginBuildEmail($emailTemplate), ); $PhpMailer->_phpMailerPluginSendEmail($sendEmail); $this->setAPIResponse('success', 'User password has been reset and email has been sent', 200); } return true; } else { $this->setAPIResponse('error', 'User not found', 404); return false; } } public function register($array) { $email = $array['email'] ?? null; $username = $array['username'] ?? null; $password = $array['password'] ?? null; $registrationPassword = $array['registrationPassword'] ?? null; if (!$email) { $this->setAPIResponse('error', 'Email was not supplied', 422); return false; } if (!$username) { $this->setAPIResponse('error', 'Username was not supplied', 422); return false; } if (!$password) { $this->setAPIResponse('error', 'Password was not supplied', 422); return false; } if (!$registrationPassword) { $this->setAPIResponse('error', 'Registration Password was not supplied', 422); return false; } $this->setLoggerChannel('User Registration'); if ($registrationPassword == $this->decrypt($this->config['registrationPassword'])) { $this->logger->debug('Registration Password Verified'); if ($this->createUser($username, $password, $email)) { $this->logger->info('A User has registered'); if ($this->createToken($username, $email, $this->config['rememberMeDays'])) { $this->setLoggerChannel('Authentication', $username); $this->logger->info('User has logged in'); return true; } } else { return false; } } else { $this->logger->warning('Wrong Password'); $this->setAPIResponse('error', 'Registration Password was incorrect', 401); return false; } } public function authRegister($username, $password, $email, $token = null) { $this->setLoggerChannel('Authentication', $username); if ($this->config['authBackend'] !== '') { $this->ombiImport($this->config['authBackend']); } $this->ssoCheck($username, $password, $token); if ($token && (!$password || $password == '')) { $password = $this->random_ascii_string(10); } if ($this->createUser($username, $password, $email)) { $this->logger->info('A User has registered'); if ($this->config['PHPMAILER-enabled'] && $email !== '') { $PhpMailer = new PhpMailer(); $emailTemplate = array( 'type' => 'registration', 'body' => $this->config['PHPMAILER-emailTemplateRegisterUser'], 'subject' => $this->config['PHPMAILER-emailTemplateRegisterUserSubject'], 'user' => $username, 'password' => null, 'inviteCode' => null, ); $emailTemplate = $PhpMailer->_phpMailerPluginEmailTemplate($emailTemplate); $sendEmail = array( 'to' => $email, 'user' => $username, 'subject' => $emailTemplate['subject'], 'body' => $PhpMailer->_phpMailerPluginBuildEmail($emailTemplate), ); $PhpMailer->_phpMailerPluginSendEmail($sendEmail); } if ($this->createToken($username, $email, $this->config['rememberMeDays'])) { $this->logger->info('User has logged in'); return true; } else { return false; } } else { $this->logger->warning('Registration error occurred'); return false; } } public function revokeTokenCurrentUser($token = null) { if ($token) { $response = [ array( 'function' => 'query', 'query' => array( 'DELETE FROM tokens WHERE token = ?', [$token] ) ), ]; } else { $response = [ array( 'function' => 'query', 'query' => array( 'DELETE FROM tokens WHERE user_id = ?', [$this->user['userID']] ) ), ]; } return $this->processQueries($response); } public function revokeToken($token = null) { if (!$token) { $this->setAPIResponse('error', 'Token was not supplied', 422); return false; } $response = [ array( 'function' => 'query', 'query' => array( 'DELETE FROM tokens WHERE token = ?', [$token] ) ), ]; $this->setAPIResponse('success', 'Token revoked', 204); return $this->processQueries($response); } public function revokeTokenByIdCurrentUser($id = null) { if (!$id) { $this->setAPIResponse('error', 'Id was not supplied', 422); return false; } $response = [ array( 'function' => 'query', 'query' => array( 'DELETE FROM tokens WHERE id = ? AND user_id = ?', $id, $this->user['userID'] ) ), ]; $this->setAPIResponse('success', 'Token revoked', 204); return $this->processQueries($response); } public function revokeTokensByUserId($userId = null) { if (!$userId) { $this->setAPIResponse('error', 'User Id was not supplied', 422); return false; } $response = [ array( 'function' => 'query', 'query' => array( 'DELETE FROM tokens WHERE user_id = ?', $userId, ) ), ]; $this->setAPIResponse('success', 'User Tokens revoked', 204); $this->setLoggerChannel('User Management')->info('Revoked all tokens for deleted user', ['id' => $userId]); return $this->processQueries($response); } public function updateUserPassword($password, $id) { $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE users SET', ['password' => password_hash($password, PASSWORD_BCRYPT)], 'WHERE id = ?', $id ) ), ]; return $this->processQueries($response); } public function updateUserPlexToken($token, $id) { $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE users SET', ['plex_token' => $token], 'WHERE id = ?', $id ) ), ]; return $this->processQueries($response); } public function getUserTabsAndCategories($type = null) { if (!$this->hasDB()) { return false; } $sort = ($this->config['unsortedTabs'] == 'top') ? 'DESC' : 'ASC'; $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM tabs WHERE `group_id` >= ? AND `group_id_max` <= ? AND `enabled` = 1 ORDER BY `order` ' . $sort, $this->user['groupID'], $this->user['groupID'], ), 'key' => 'tabs' ), array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM tabs WHERE `add_to_admin` = 1 AND `enabled` = 1 ORDER BY `order` ' . $sort ), 'key' => 'tabs-admin' ), array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM categories ORDER BY `order` ASC', ), 'key' => 'categories' ), ]; $queries = $this->processQueries($response); $this->applyTabVariables($queries['tabs']); if ($this->qualifyRequest(1)) { $this->applyTabVariables($queries['tabs-admin']); $all['tabs'] = array_merge($queries['tabs'], $queries['tabs-admin']); $newArray = []; $ids = []; foreach ($all['tabs'] as $key => $line) { if (!in_array($line['id'], $ids)) { $ids[] = $line['id']; $newArray[$key] = $line; } } $all['tabs'] = $newArray; if (count($all['tabs']) > 0) { usort($all['tabs'], function ($a, $b) { if ($this->config['unsortedTabs'] == 'top') { return $b['order'] <=> $a['order']; } else { return $a['order'] <=> $b['order']; } }); } $newArray = NULL; $ids = NULL; } else { $all['tabs'] = $queries['tabs']; } foreach ($all['tabs'] as $k => $v) { $v['url_local'] = $v['type'] !== 0 ? $this->checkTabURL($v['url_local']) : $v['url_local']; $v['url'] = $v['type'] !== 0 ? $this->checkTabURL($v['url']) : $v['url']; $v['access_url'] = (!empty($v['url_local']) && ($v['url_local'] !== null) && ($v['url_local'] !== 'null') && $this->isLocal() && $v['type'] !== 0) ? $v['url_local'] : $v['url']; } $count = array_map(function ($element) { return $element['category_id']; }, $all['tabs']); $count = (array_count_values($count)); foreach ($queries['categories'] as $k => $v) { $v['count'] = $count[$v['category_id']] ?? 0; } $all['categories'] = $queries['categories']; switch ($type) { case 'categories': return $all['categories']; case 'tabs': return $all['tabs']; default: return $all; } } public function checkTabURL($url = null) { return $url !== '' && $url !== null & $url !== 'null' ? $this->qualifyURL($url, false, true) : ''; } public function refreshList() { $searchTerm = "Refresh"; $list = array_filter($this->config, function ($k) use ($searchTerm) { return stripos($k, $searchTerm) !== false; }, ARRAY_FILTER_USE_KEY); foreach ($list as $item => $value) { if (!is_numeric($value)) { unset($list[$item]); } } return $list; } public function homepageOrderList() { $searchTerm = "homepageOrder"; $order = array_filter($this->config, function ($k) use ($searchTerm) { return stripos($k, $searchTerm) !== false; }, ARRAY_FILTER_USE_KEY); asort($order); return $order; } public function tautulliList() { $searchTerm = "tautulli_token"; return array_filter($this->config, function ($k) use ($searchTerm) { return stripos($k, $searchTerm) !== false; }, ARRAY_FILTER_USE_KEY); } public function checkPlexAdminFilled() { if ($this->config['plexAdmin'] == '') { return false; } else { if ((strpos($this->config['plexAdmin'], '@') !== false)) { return 'email'; } else { return 'username'; } } } public function organizrSpecialSettings() { // js activeInfo return [ 'homepage' => [ 'refresh' => $this->refreshList(), 'order' => $this->homepageOrderList(), 'search' => [ 'enabled' => $this->qualifyRequest($this->config['mediaSearchAuth']) && $this->config['mediaSearch'] == true && $this->config['plexToken'], 'type' => $this->config['mediaSearchType'], ], 'requests' => [ 'service' => $this->config['defaultRequestService'], ], 'ombi' => [ 'enabled' => $this->qualifyRequest($this->config['homepageOmbiAuth']) && $this->qualifyRequest($this->config['homepageOmbiRequestAuth']) && $this->config['homepageOmbiEnabled'] == true && $this->config['ssoOmbi'] && isset($_COOKIE['Auth']), 'authView' => $this->qualifyRequest($this->config['homepageOmbiAuth']), 'authRequest' => $this->qualifyRequest($this->config['homepageOmbiRequestAuth']), 'sso' => (bool)$this->config['ssoOmbi'], 'cookie' => isset($_COOKIE['Auth']), 'alias' => (bool)$this->config['ombiAlias'], 'ombiDefaultFilterAvailable' => (bool)$this->config['ombiDefaultFilterAvailable'], 'ombiDefaultFilterUnavailable' => (bool)$this->config['ombiDefaultFilterUnavailable'], 'ombiDefaultFilterApproved' => (bool)$this->config['ombiDefaultFilterApproved'], 'ombiDefaultFilterUnapproved' => (bool)$this->config['ombiDefaultFilterUnapproved'], 'ombiDefaultFilterDenied' => (bool)$this->config['ombiDefaultFilterDenied'] ], 'overseerr' => [ 'enabled' => $this->qualifyRequest($this->config['homepageOverseerrAuth']) && $this->qualifyRequest($this->config['homepageOverseerrRequestAuth']) && $this->config['homepageOverseerrEnabled'] == true && $this->config['ssoOverseerr'] && isset($_COOKIE['connect_sid']), 'authView' => $this->qualifyRequest($this->config['homepageOverseerrAuth']), 'authRequest' => $this->qualifyRequest($this->config['homepageOverseerrRequestAuth']), 'sso' => (bool)$this->config['ssoOverseerr'], 'cookie' => isset($_COOKIE['connect_sid']), 'userSelectTv' => (bool)$this->config['homepageOverseerrRequestAuth'] == 'user', 'overseerrDefaultFilterAvailable' => (bool)$this->config['overseerrDefaultFilterAvailable'], 'overseerrDefaultFilterUnavailable' => (bool)$this->config['overseerrDefaultFilterUnavailable'], 'overseerrDefaultFilterApproved' => (bool)$this->config['overseerrDefaultFilterApproved'], 'overseerrDefaultFilterUnapproved' => (bool)$this->config['overseerrDefaultFilterUnapproved'], 'overseerrDefaultFilterDenied' => (bool)$this->config['overseerrDefaultFilterDenied'] ], 'jackett' => [ 'homepageJackettBackholeDownload' => $this->config['homepageJackettBackholeDownload'] ? true : false ], 'prowlarr' => [ 'homepageProwlarrBackholeDownload' => $this->config['homepageProwlarrBackholeDownload'] ? true : false ], 'options' => [ 'alternateHomepageHeaders' => $this->config['alternateHomepageHeaders'], 'healthChecksTags' => $this->config['healthChecksTags'], 'titles' => [ 'tautulli' => $this->config['tautulliHeader'] ] ], 'media' => [ 'jellyfin' => $this->config['homepageJellyfinInstead'] ] ], 'sso' => [ 'misc' => [ 'oAuthLogin' => isset($_COOKIE['oAuth']), 'rememberMe' => $this->config['rememberMe'], 'rememberMeDays' => $this->config['rememberMeDays'] ], 'plex' => [ 'enabled' => (bool)$this->config['ssoPlex'], 'cookie' => isset($_COOKIE['mpt']), 'machineID' => strlen($this->config['plexID']) == 40, 'token' => $this->config['plexToken'] !== '', 'plexAdmin' => $this->checkPlexAdminFilled(), 'strict' => (bool)$this->config['plexStrictFriends'], 'oAuthEnabled' => (bool)$this->config['plexoAuth'], 'backend' => $this->config['authBackend'] == 'plex', ], 'tautulli' => [ 'enabled' => (bool)$this->config['ssoTautulli'], 'cookie' => !empty($this->tautulliList()), 'url' => ($this->config['tautulliURL'] !== '') ? $this->config['tautulliURL'] : false, ], 'overseerr' => [ 'enabled' => (bool)$this->config['ssoOverseerr'], 'cookie' => isset($_COOKIE['connect.sid']), 'url' => ($this->config['overseerrURL'] !== '') ? $this->config['overseerrURL'] : false, 'api' => $this->config['overseerrToken'] !== '', ], 'petio' => [ 'enabled' => (bool)$this->config['ssoPetio'], 'cookie' => isset($_COOKIE['petio_jwt']), 'url' => ($this->config['petioURL'] !== '') ? $this->config['petioURL'] : false, 'api' => $this->config['petioToken'] !== '', ], 'ombi' => [ 'enabled' => (bool)$this->config['ssoOmbi'], 'cookie' => isset($_COOKIE['Auth']), 'url' => ($this->config['ombiURL'] !== '') ? $this->config['ombiURL'] : false, 'api' => $this->config['ombiToken'] !== '', ], 'jellyfin' => [ 'enabled' => (bool)$this->config['ssoJellyfin'], 'url' => ($this->config['jellyfinURL'] !== '') ? $this->config['jellyfinURL'] : false, 'ssoUrl' => ($this->config['jellyfinSSOURL'] !== '') ? $this->config['jellyfinSSOURL'] : false, ], 'komga' => [ 'enabled' => (bool)$this->config['ssoKomga'], 'cookie' => isset($_COOKIE['komga_token']), 'url' => ($this->config['komgaURL'] !== '') ? $this->config['komgaURL'] : false, ] ], 'ping' => [ 'onlineSound' => $this->config['pingOnlineSound'], 'offlineSound' => $this->config['pingOfflineSound'], 'statusSounds' => $this->config['statusSounds'], 'auth' => $this->config['pingAuth'], 'authMessage' => $this->config['pingAuthMessage'], 'authMs' => $this->config['pingAuthMs'], 'ms' => $this->config['pingMs'], 'adminRefresh' => $this->config['adminPingRefresh'], 'everyoneRefresh' => $this->config['otherPingRefresh'], ], 'notifications' => [ 'backbone' => $this->config['notificationBackbone'], 'position' => $this->config['notificationPosition'] ], 'lockout' => [ 'enabled' => $this->config['lockoutSystem'], 'timer' => $this->config['lockoutTimeout'], 'minGroup' => $this->config['lockoutMinAuth'], 'maxGroup' => $this->config['lockoutMaxAuth'] ], 'user' => [ 'agent' => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null, 'oAuthLogin' => isset($_COOKIE['oAuth']), 'local' => $this->isLocal(), 'ip' => $this->userIP() ], 'login' => [ 'rememberMe' => $this->config['rememberMe'], 'rememberMeDays' => $this->config['rememberMeDays'], 'wanDomain' => $this->config['wanDomain'], 'localAddress' => $this->config['localAddress'], 'enableLocalAddressForward' => $this->config['enableLocalAddressForward'], ], 'misc' => [ 'installedPlugins' => $this->qualifyRequest(1) ? $this->config['installedPlugins'] : '', 'installedThemes' => $this->qualifyRequest(1) ? $this->config['installedThemes'] : '', 'return' => $_SERVER['HTTP_REFERER'] ?? false, 'authDebug' => $this->config['authDebug'], 'minimalLoginScreen' => $this->config['minimalLoginScreen'], 'unsortedTabs' => $this->config['unsortedTabs'], 'authType' => $this->config['authType'], 'authBackend' => $this->config['authBackend'], 'newMessageSound' => (isset($this->config['CHAT-newMessageSound-include'])) ? $this->config['CHAT-newMessageSound-include'] : '', 'uuid' => ($this->config['uuid']) ?? null, 'docker' => $this->qualifyRequest(1) ? $this->docker : '', 'githubCommit' => $this->qualifyRequest(1) ? $this->commit : '', 'schema' => $this->qualifyRequest(1) ? $this->getSchema() : '', 'debugArea' => $this->qualifyRequest($this->config['debugAreaAuth']), 'debugErrors' => $this->config['debugErrors'], 'sandbox' => $this->config['sandbox'], 'iframeAllow' => $this->config['iframeAllow'], 'expandCategoriesByDefault' => $this->config['expandCategoriesByDefault'], 'autoCollapseCategories' => $this->config['autoCollapseCategories'], 'autoExpandNavBar' => $this->config['autoExpandNavBar'], 'sideMenuCollapsed' => $this->config['allowCollapsableSideMenu'] && $this->config['sideMenuCollapsed'], 'collapseSideMenuOnClick' => $this->config['allowCollapsableSideMenu'] && $this->config['collapseSideMenuOnClick'], 'authProxyOverrideLogout' => $this->config['authProxyOverrideLogout'], 'authProxyLogoutURL' => $this->config['authProxyLogoutURL'], 'disableHomepageModals' => $this->config['disableHomepageModals'], 'checkForUpdate' => $this->config['checkForUpdate'] ], 'menuLink' => [ 'githubMenuLink' => $this->config['githubMenuLink'], 'organizrSupportMenuLink' => $this->config['organizrSupportMenuLink'], 'organizrDocsMenuLink' => $this->config['organizrDocsMenuLink'], 'organizrSignoutMenuLink' => $this->config['organizrSignoutMenuLink'], 'organizrFeatureRequestLink' => $this->config['organizrFeatureRequestLink'] ] ]; } public function checkLog($path) { if (file_exists($path)) { if (filesize($path) > 500000) { rename($path, $path . '[' . date('Y-m-d') . '].json'); return false; } return true; } else { return false; } } public function isApprovedRequest($method, $data) { $requesterToken = $this->getallheadersi()['token'] ?? ($_GET['apikey'] ?? false); $apiKey = ($this->config['organizrAPI']) ?? null; if (isset($data['formKey'])) { $formKey = $data['formKey']; } elseif (isset($this->getallheadersi()['formkey'])) { $formKey = $this->getallheadersi()['formkey']; } else { $formKey = false; } // Check token or API key // If API key, return 0 for admin if (strlen($requesterToken) == 20 && $requesterToken == $apiKey) { //DO API CHECK return true; } elseif ($method == 'POST') { if ($this->checkFormKey($formKey)) { return true; } else { $this->setLoggerChannel('Authentication'); $this->logger->warning('Unable to authenticate Form Key: ' . $formKey); return false; } } else { return true; } return false; } public function checkFormKey($formKey = '') { return password_verify(substr($this->config['organizrHash'], 2, 10), $formKey); } public function buildHomepage() { $homepageOrder = $this->homepageOrderList(); $homepageBuilt = ''; foreach ($homepageOrder as $key => $value) { //new way if (method_exists($this, $key)) { $homepageBuilt .= $this->$key(); } elseif (strpos($key, 'homepageOrdercustomhtml') !== false) { $iteration = substr($key, -2); $homepageBuilt .= $this->homepageOrdercustomhtml($iteration); } else { $homepageBuilt .= '
'; } //old way //$homepageBuilt .= $this->buildHomepageItem($key); } return $homepageBuilt; } public function buildHomepageSettings() { $homepageOrder = $this->homepageOrderList(); $homepageList = '

Drag Homepage Items to Order Them

'; $inputList = '
'; foreach ($homepageOrder as $key => $val) { switch ($key) { case 'homepageOrdercustomhtml01': case 'homepageOrdercustomhtml02': case 'homepageOrdercustomhtml03': case 'homepageOrdercustomhtml04': case 'homepageOrdercustomhtml05': case 'homepageOrdercustomhtml06': case 'homepageOrdercustomhtml07': case 'homepageOrdercustomhtml08': $iteration = substr($key, -2); $class = 'bg-info'; $image = 'plugins/images/tabs/HTML5.png'; if (!$this->config['homepageCustomHTML' . $iteration . 'Enabled']) { $class .= ' faded'; } break; case 'homepageOrdertransmission': $class = 'bg-transmission'; $image = 'plugins/images/tabs/transmission.png'; if (!$this->config['homepageTransmissionEnabled']) { $class .= ' faded'; } break; case 'homepageOrdernzbget': $class = 'bg-nzbget'; $image = 'plugins/images/tabs/nzbget.png'; if (!$this->config['homepageNzbgetEnabled']) { $class .= ' faded'; } break; case 'homepageOrderjdownloader': $class = 'bg-sab'; $image = 'plugins/images/tabs/jdownloader.png'; if (!$this->config['homepageJdownloaderEnabled']) { $class .= ' faded'; } break; case 'homepageOrdersabnzbd': $class = 'bg-sab'; $image = 'plugins/images/tabs/sabnzbd.png'; if (!$this->config['homepageSabnzbdEnabled']) { $class .= ' faded'; } break; case 'homepageOrderdeluge': $class = 'bg-deluge'; $image = 'plugins/images/tabs/deluge.png'; if (!$this->config['homepageDelugeEnabled']) { $class .= ' faded'; } break; case 'homepageOrderqBittorrent': $class = 'bg-qbit'; $image = 'plugins/images/tabs/qBittorrent.png'; if (!$this->config['homepageqBittorrentEnabled']) { $class .= ' faded'; } break; case 'homepageOrderuTorrent': $class = 'bg-qbit'; $image = 'plugins/images/tabs/utorrent.png'; if (!$this->config['homepageuTorrentEnabled']) { $class .= ' faded'; } break; case 'homepageOrderrTorrent': $class = 'bg-qbit'; $image = 'plugins/images/tabs/rTorrent.png'; if (!$this->config['homepagerTorrentEnabled']) { $class .= ' faded'; } break; case 'homepageOrderplexnowplaying': case 'homepageOrderplexrecent': case 'homepageOrderplexplaylist': $class = 'bg-plex'; $image = 'plugins/images/tabs/plex.png'; if (!$this->config['homepagePlexEnabled']) { $class .= ' faded'; } break; case 'homepageOrderembynowplaying': case 'homepageOrderembyrecent': $class = 'bg-emby'; $image = 'plugins/images/tabs/emby.png'; if (!$this->config['homepageEmbyEnabled']) { $class .= ' faded'; } break; case 'homepageOrderjellyfinnowplaying': case 'homepageOrderjellyfinrecent': $class = 'bg-jellyfin'; $image = 'plugins/images/tabs/jellyfin.png'; if (!$this->config['homepageJellyfinEnabled']) { $class .= ' faded'; } break; case 'homepageOrderombi': $class = 'bg-inverse'; $image = 'plugins/images/tabs/ombi.png'; if (!$this->config['homepageOmbiEnabled']) { $class .= ' faded'; } break; case 'homepageOrderoverseerr': $class = 'bg-inverse'; $image = 'plugins/images/tabs/overseerr.png'; if (!$this->config['homepageOverseerrEnabled']) { $class .= ' faded'; } break; case 'homepageOrderDonate': $class = 'bg-primary'; $image = 'plugins/images/tabs/donate.png'; if (!$this->config['homepageDonateEnabled']) { $class .= ' faded'; } break; case 'homepageOrdercalendar': $class = 'bg-primary'; $image = 'plugins/images/tabs/calendar.png'; if (!$this->config['homepageCalendarEnabled'] && !$this->config['homepageSonarrEnabled'] && !$this->config['homepageRadarrEnabled'] && !$this->config['homepageSickrageEnabled'] && !$this->config['homepageCouchpotatoEnabled']) { $class .= ' faded'; } break; case 'homepageOrderdownloader': $class = 'bg-inverse'; $image = 'plugins/images/tabs/downloader.png'; if (!$this->config['jdownloaderCombine'] && !$this->config['sabnzbdCombine'] && !$this->config['nzbgetCombine'] && !$this->config['rTorrentCombine'] && !$this->config['delugeCombine'] && !$this->config['transmissionCombine'] && !$this->config['qBittorrentCombine'] && !$this->config['uTorrentCombine']) { $class .= ' faded'; } break; case 'homepageOrderhealthchecks': $class = 'bg-healthchecks'; $image = 'plugins/images/tabs/healthchecks.png'; if (!$this->config['homepageHealthChecksEnabled']) { $class .= ' faded'; } break; case 'homepageOrderunifi': $class = 'bg-info'; $image = 'plugins/images/tabs/ubnt.png'; if (!$this->config['homepageUnifiEnabled']) { $class .= ' faded'; } break; case 'homepageOrdertautulli': $class = 'bg-info'; $image = 'plugins/images/tabs/tautulli.png'; if (!$this->config['homepageTautulliEnabled']) { $class .= ' faded'; } break; case 'homepageOrderPihole': $class = 'bg-info'; $image = 'plugins/images/tabs/pihole.png'; if (!$this->config['homepagePiholeEnabled']) { $class .= ' faded'; } break; case 'homepageOrderAdGuard': $class = 'bg-info'; $image = 'plugins/images/tabs/AdGuardHomepageItem'; if (!$this->config['homepageAdGuardEnabled']) { $class .= ' faded'; } break; case 'homepageOrderMonitorr': $class = 'bg-info'; $image = 'plugins/images/tabs/monitorr.png'; if (!$this->config['homepageMonitorrEnabled']) { $class .= ' faded'; } break; case 'homepageOrderUptimeKuma': $class = 'bg-info'; $image = 'plugins/images/tabs/kuma.png'; if (!$this->config['homepageUptimeKumaEnabled']) { $class .= ' faded'; } break; case 'homepageOrderWeatherAndAir': $class = 'bg-success'; $image = 'plugins/images/tabs/wind.png'; if (!$this->config['homepageWeatherAndAirEnabled']) { $class .= ' faded'; } break; case 'homepageOrderSpeedtest': $class = 'bg-success'; $image = 'plugins/images/tabs/speedtest-icon.png'; if (!$this->config['homepageSpeedtestEnabled']) { $class .= ' faded'; } break; case 'homepageOrderNetdata': $class = 'bg-success'; $image = 'plugins/images/tabs/netdata.png'; if (!$this->config['homepageNetdataEnabled']) { $class .= ' faded'; } break; case 'homepageOrderOctoprint': $class = 'bg-success'; $image = 'plugins/images/tabs/octoprint.png'; if (!$this->config['homepageOctoprintEnabled']) { $class .= ' faded'; } break; case 'homepageOrderSonarrQueue': $class = 'bg-sonarr'; $image = 'plugins/images/tabs/sonarr.png'; if (!$this->config['homepageSonarrQueueEnabled']) { $class .= ' faded'; } break; case 'homepageOrderRadarrQueue': $class = 'bg-radarr'; $image = 'plugins/images/tabs/radarr.png'; if (!$this->config['homepageRadarrQueueEnabled']) { $class .= ' faded'; } break; case 'homepageOrderJackett': $class = 'bg-inverse'; $image = 'plugins/images/tabs/jackett.png'; if (!$this->config['homepageJackettEnabled']) { $class .= ' faded'; } break; case 'homepageOrderProwlarr': $class = 'bg-inverse'; $image = 'plugins/images/tabs/prowlarr.png'; if (!$this->config['homepageProwlarrEnabled']) { $class .= ' faded'; } break; case 'homepageOrderBookmarks': $class = 'bg-bookmarks'; $image = 'plugins/images/bookmark.png'; if (!$this->config['homepageBookmarksEnabled']) { $class .= ' faded'; } break; default: $class = 'blue-bg'; $image = ''; break; } $homepageList .= '
' . $val . '   ' . strtoupper(substr($key, 13)) . '
'; $inputList .= ''; } $homepageList .= '
'; $inputList .= ''; return $homepageList . $inputList; } public function setGroupOptionsVariable() { $this->groupOptions = $this->groupSelect(); } public function getSettingsHomepageItem($item) { $items = $this->getSettingsHomepage(); foreach ($items as $k => $v) { if (strtolower($v['name']) === strtolower($item)) { $functionName = $v['settingsArray']; return $this->$functionName(); } } $this->setAPIResponse('error', 'Homepage item was not found', 404); return null; } public function getSettingsHomepageItemDebug($service) { $service = $this->getSettingsHomepageItem($service); if ($service) { $debug = []; foreach ($service['settings'] as $category => $items) { if ($category !== 'About' && $category !== 'Test Connection') { foreach ($items as $item) { if ($item['type'] !== 'html' && $item['type'] !== 'blank' && $item['type'] !== 'button') { if ((stripos($item['name'], 'token') !== false) || (stripos($item['name'], 'key') !== false) || (stripos($item['name'], 'password'))) { if ($item['value'] !== '') { $item['value'] = '***redacted***'; } } $debug[$category][$item['name']] = $item['value']; } } } } return $debug; } $this->setAPIResponse('error', 'Homepage item was not found', 404); return null; } public function getSettingsHomepage() { $this->setGroupOptionsVariable(); return $this->getHomepageSettingsCombined(); } public function isTabNameTaken($name, $id = null) { if ($id) { $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM tabs WHERE `name` LIKE ? AND `id` != ?', $name, $id ) ), ]; } else { $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM tabs WHERE `name` LIKE ?', $name ) ), ]; } return $this->processQueries($response); } public function isCategoryNameTaken($name, $id = null) { if ($id) { $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM categories WHERE `category` LIKE ? AND `id` != ?', $name, $id ) ), ]; } else { $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM categories WHERE `category` LIKE ?', $name ) ), ]; } return $this->processQueries($response); } public function isGroupNameTaken($name, $id = null) { if ($id) { $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM groups WHERE `group` LIKE ? AND `id` != ?', $name, $id ) ), ]; } else { $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM groups WHERE `group` LIKE ?', $name ) ), ]; } return $this->processQueries($response); } public function getTableColumns($table) { if ($this->config['driver'] == 'sqlite3') { $query = 'PRAGMA table_info(?)'; } else { $query = 'DESCRIBE %n'; } $response = [ array( 'function' => 'fetchAll', 'query' => [ $query, $table ] ), ]; return $this->processQueries($response); } public function getTableColumnsFormatted($table) { if ($this->config['driver'] == 'sqlite3') { $name = 'name'; } else { $name = 'Field'; } $columns = $this->getTableColumns($table); if ($columns) { $columnsFormatted = []; foreach ($columns as $k => $v) { $columnsFormatted[$v[$name]] = $v; } return $columnsFormatted; } else { return false; } } public function getTabById($id) { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM tabs WHERE `id` = ?', $id ) ), ]; return $this->processQueries($response); } public function getTabGroupByTabName($tab) { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT group_id FROM tabs WHERE name LIKE %~like~', $tab ) ), ]; $query = $this->processQueries($response); return $query ? $query['group_id'] : 0; } public function getCategoryById($id) { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM categories WHERE `id` = ?', $id ) ), ]; return $this->processQueries($response); } public function getGroupUserCountById($id) { $response = [ array( 'function' => 'fetchSingle', 'query' => array( 'SELECT count(username) AS count FROM groups INNER JOIN users ON users.group_id = groups.group_id AND groups.id = ?', $id ) ), ]; return $this->processQueries($response); } public function getGroupById($id) { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM groups WHERE `id` = ?', $id ) ), ]; return $this->processQueries($response); } public function getGroupByGroupId($id) { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM groups WHERE `group_id` = ?', $id ) ), ]; return $this->processQueries($response); } public function getDefaultGroup() { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM groups WHERE `default` = 1' ) ), ]; return $this->processQueries($response); } public function getDefaultGroupId() { $response = [ array( 'function' => 'fetchSingle', 'query' => array( 'SELECT `group_id` FROM groups WHERE `default` = 1' ) ), ]; return $this->processQueries($response); } public function getDefaultCategory() { $response = [ array( 'function' => 'fetch', 'query' => array( 'SELECT * FROM categories WHERE `default` = 1' ) ), ]; return $this->processQueries($response); } public function getDefaultCategoryId() { $response = [ array( 'function' => 'fetchSingle', 'query' => array( 'SELECT `category_id` FROM categories WHERE `default` = 1' ) ), ]; return $this->processQueries($response); } public function getNextTabOrder() { $response = [ array( 'function' => 'fetchSingle', 'query' => array( 'SELECT `order` from tabs ORDER BY `order` DESC' ) ), ]; return $this->processQueries($response); } public function getNextCategoryOrder() { $response = [ array( 'function' => 'fetchSingle', 'query' => array( 'SELECT `order` from categories ORDER BY `order` DESC' ) ), ]; return $this->processQueries($response); } public function getNextGroupOrder() { $response = [ array( 'function' => 'fetchSingle', 'query' => array( 'SELECT `group_id` from groups WHERE `group_id` != "999" ORDER BY `group_id` DESC' ) ), ]; return $this->processQueries($response); } public function getNextCategoryId() { $response = [ array( 'function' => 'fetchSingle', 'query' => array( 'SELECT `category_id` from categories ORDER BY `category_id` DESC' ) ), ]; return $this->processQueries($response); } public function clearTabDefault() { $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE tabs SET `default` = 0' ) ), ]; return $this->processQueries($response); } public function clearCategoryDefault() { $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE categories SET `default` = 0' ) ), ]; return $this->processQueries($response); } public function clearGroupDefault() { $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE groups SET `default` = 0' ) ), ]; return $this->processQueries($response); } public function checkKeys($tabInfo, $newData) { foreach ($newData as $k => $v) { if (!array_key_exists($k, $tabInfo)) { unset($newData[$k]); } } return $newData; } public function getTabByIdCheckUser($id) { $tabInfo = $this->getTabById($id); if ($tabInfo) { if ($this->qualifyRequest($tabInfo['group_id'], true)) { return $tabInfo; } } else { $this->setAPIResponse('error', 'id not found', 404); return false; } } public function deleteTab($id) { $response = [ array( 'function' => 'query', 'query' => array( 'DELETE FROM tabs WHERE id = ?', $id ) ), ]; $tabInfo = $this->getTabById($id); if ($tabInfo) { $this->setLoggerChannel('Tab Management'); $this->logger->debug('Deleted Tab [' . $tabInfo['name'] . ']'); $this->setAPIResponse('success', 'Tab deleted', 204); return $this->processQueries($response); } else { $this->setAPIResponse('error', 'id not found', 404); return false; } } public function addTab($array) { if (!$array) { $this->setAPIResponse('error', 'no data was sent', 422); return null; } $array = $this->checkKeys($this->getTableColumnsFormatted('tabs'), $array); $array['group_id'] = ($array['group_id']) ?? $this->getDefaultGroupId(); $array['category_id'] = ($array['category_id']) ?? $this->getDefaultCategoryId(); $array['enabled'] = ($array['enabled']) ?? 0; $array['default'] = ($array['default']) ?? 0; $array['type'] = ($array['type']) ?? 1; $array['order'] = ($array['order']) ?? $this->getNextTabOrder() + 1; if (array_key_exists('name', $array)) { $array['name'] = $this->sanitizeUserString($array['name']); if ($this->isTabNameTaken($array['name'])) { $this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409); return false; } if (!$this->qualifyLength($array['name'], 50, true)) { return false; } } else { $this->setAPIResponse('error', 'Tab name was not supplied', 422); return false; } if (!array_key_exists('url', $array) && !array_key_exists('url_local', $array)) { $this->setAPIResponse('error', 'Tab url or url_local was not supplied', 422); return false; } if (!array_key_exists('image', $array)) { $this->setAPIResponse('error', 'Tab image was not supplied', 422); return false; } else { $array['image'] = $this->sanitizeUserString($array['image']); } $response = [ array( 'function' => 'query', 'query' => array( 'INSERT INTO [tabs]', $array ) ), ]; $this->setAPIResponse(null, 'Tab added'); $this->setLoggerChannel('Tab Management'); $this->logger->debug('Added Tab for [' . $array['name'] . ']'); return $this->processQueries($response); } public function updateTab($id, $array) { if (!$id || $id == '') { $this->setAPIResponse('error', 'id was not set', 422); return null; } if (!$array) { $this->setAPIResponse('error', 'no data was sent', 422); return null; } $tabInfo = $this->getTabById($id); if ($tabInfo) { $array = $this->checkKeys($tabInfo, $array); } else { $this->setAPIResponse('error', 'No tab info found', 404); return false; } if (array_key_exists('name', $array)) { $array['name'] = $this->sanitizeUserString($array['name']); if ($this->isTabNameTaken($array['name'], $id)) { $this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409); return false; } if (!$this->qualifyLength($array['name'], 50, true)) { return false; } } if (array_key_exists('default', $array)) { if ($array['default']) { $this->clearTabDefault(); } } if (array_key_exists('image', $array)) { $array['image'] = $this->sanitizeUserString($array['image']); } if (array_key_exists('group_id', $array)) { $groupCheck = (array_key_exists('group_id_max', $array)) ? $array['group_id_max'] : $tabInfo['group_id_max']; if ($array['group_id'] < $groupCheck) { $this->setAPIResponse('error', 'Tab name: ' . $tabInfo['name'] . ' cannot have a lower Group Id Max than Group Id Min', 409); return false; } } if (array_key_exists('group_id_max', $array)) { $groupCheck = (array_key_exists('group_id', $array)) ? $array['group_id'] : $tabInfo['group_id']; if ($array['group_id_max'] > $groupCheck) { $this->setAPIResponse('error', 'Tab name: ' . $tabInfo['name'] . ' cannot have a higher Group Id Min than Group Id Max', 409); return false; } } $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE tabs SET', $array, 'WHERE id = ?', $id ) ), ]; $this->setAPIResponse(null, 'Tab info updated'); $this->setLoggerChannel('Tab Management'); $this->logger->debug('Edited Tab Info for [' . $tabInfo['name'] . ']'); return $this->processQueries($response); } public function updateTabOrder($array) { if (count($array) >= 1) { foreach ($array as $tab) { if (count($tab) !== 2) { $this->setAPIResponse('error', 'data is malformed', 422); break; } $id = $tab['id'] ?? null; $order = $tab['order'] ?? null; if ($id && $order) { $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE tabs set `order` = ? WHERE `id` = ?', $order, $id ) ), ]; $this->processQueries($response); $this->setAPIResponse(null, 'Tab Order updated'); } else { $this->setAPIResponse('error', 'data is malformed', 422); } } } else { $this->setAPIResponse('error', 'data is empty or not in array', 422); return false; } } public function addCategory($array) { if (!$array) { $this->setAPIResponse('error', 'no data was sent', 422); return null; } $array = $this->checkKeys($this->getTableColumnsFormatted('categories'), $array); $array['default'] = ($array['default']) ?? 0; $array['order'] = ($array['order']) ?? $this->getNextCategoryOrder() + 1; $array['category_id'] = ($array['category_id']) ?? $this->getNextCategoryId() + 1; if (array_key_exists('category', $array)) { $array['category'] = $this->sanitizeUserString($array['category']); if ($this->isCategoryNameTaken($array['category'])) { $this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409); return false; } if (!$this->qualifyLength($array['category'], 50, true)) { return false; } } else { $this->setAPIResponse('error', 'Category name was not supplied', 422); return false; } if (!array_key_exists('image', $array)) { $this->setAPIResponse('error', 'Category image was not supplied', 422); return false; } else { $array['image'] = $this->sanitizeUserString($array['image']); } $response = [ array( 'function' => 'query', 'query' => array( 'INSERT INTO [categories]', $array ) ), ]; $this->setAPIResponse(null, 'Category added'); $this->setLoggerChannel('Category Management'); $this->logger->debug('Added Category for [' . $array['category'] . ']'); return $this->processQueries($response); } public function updateCategory($id, $array) { if (!$id || $id == '') { $this->setAPIResponse('error', 'id was not set', 422); return null; } if (!$array) { $this->setAPIResponse('error', 'no data was sent', 422); return null; } $categoryInfo = $this->getCategoryById($id); if ($categoryInfo) { $array = $this->checkKeys($categoryInfo, $array); } else { $this->setAPIResponse('error', 'No category info found', 404); return false; } if (array_key_exists('category', $array)) { $array['category'] = $this->sanitizeUserString($array['category']); if ($this->isCategoryNameTaken($array['category'], $id)) { $this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409); return false; } if (!$this->qualifyLength($array['category'], 50, true)) { return false; } } if (array_key_exists('image', $array)) { $array['image'] = $this->sanitizeUserString($array['image']); } if (array_key_exists('default', $array)) { if ($array['default']) { $this->clearCategoryDefault(); } } $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE categories SET', $array, 'WHERE id = ?', $id ) ), ]; $this->setAPIResponse(null, 'Category info updated'); $this->setLoggerChannel('Category Management'); $this->logger->debug('Edited Category [' . $categoryInfo['category'] . ']'); return $this->processQueries($response); } public function updateCategoryOrder($array) { if (count($array) >= 1) { foreach ($array as $category) { if (count($category) !== 2) { $this->setAPIResponse('error', 'data is malformed', 422); break; } $id = $category['id'] ?? null; $order = $category['order'] ?? null; if ($id && $order) { $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE categories set `order` = ? WHERE `id` = ?', $order, $id ) ), ]; $this->processQueries($response); $this->setAPIResponse(null, 'Category Order updated'); } else { $this->setAPIResponse('error', 'data is malformed', 422); } } } else { $this->setAPIResponse('error', 'data is empty or not in array', 422); return false; } } public function deleteCategory($id) { $response = [ array( 'function' => 'query', 'query' => array( 'DELETE FROM categories WHERE id = ?', $id ) ), ]; $categoryInfo = $this->getCategoryById($id); if ($categoryInfo) { $this->setLoggerChannel('Category Management'); $this->logger->debug('Deleted Category [' . $categoryInfo['category'] . ']'); $this->setAPIResponse('success', 'Category deleted', 204); return $this->processQueries($response); } else { $this->setAPIResponse('error', 'id not found', 404); return false; } } public function inconspicuous(): string { if ($this->hasDB()) { if ($this->config['easterEggs']) { return '
'; } } return ''; } public function marketplaceFileListFormat($files, $folder, $type) { foreach ($files as $k => $v) { $splitFiles = explode('|', $v); $prePath = (strlen($k) !== 1) ? $k . '/' : $k; foreach ($splitFiles as $file) { $filesList[] = array( 'fileName' => $file, 'path' => $prePath, 'githubPath' => 'https://raw.githubusercontent.com/causefx/Organizr/v2-' . $type . '/' . $folder . $prePath . $file ); } } return $filesList; } public function removeTheme($theme) { $this->setLoggerChannel('Theme Marketplace'); $theme = $this->cleanClassName($theme, '_'); $array = $this->getThemesMarketplace(); $arrayLower = array_change_key_case($array); if (!$array) { $this->setAPIResponse('error', 'Could not access theme marketplace', 409); return false; } if (!isset($arrayLower[$theme])) { $this->setAPIResponse('error', 'Theme does not exist in marketplace', 404); return false; } else { $key = array_search($theme, array_keys($arrayLower)); $theme = array_keys($array)[$key]; } $array = $array[$theme]; $themeDir = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $array['project_folder'] . DIRECTORY_SEPARATOR; $dirExists = file_exists($themeDir); if ($dirExists) { if (!$this->rrmdir($themeDir)) { $this->logger->info('Remove File Failed for: ' . $array['project_folder']); return false; } } else { $this->setAPIResponse('error', 'Theme is not installed', 404); return false; } $this->updateInstalledThemes('uninstall', $theme, $array); $this->setAPIResponse('success', 'Theme removed', 200, $array); return true; } public function installTheme($theme) { $this->setLoggerChannel('Theme Marketplace'); $theme = $this->cleanClassName($theme, '_'); $array = $this->getThemesMarketplace(); $arrayLower = array_change_key_case($array); if (!$array) { $this->setAPIResponse('error', 'Could not access theme marketplace', 409); return false; } if (!isset($arrayLower[$theme])) { $this->setAPIResponse('error', 'Theme [' . $theme . '] does not exist in marketplace', 404, $arrayLower); return false; } else { $key = array_search($theme, array_keys($arrayLower)); $theme = array_keys($array)[$key]; } $array = $array[$theme]; // Check Version of Organizr against minimum version needed $compare = new Composer\Semver\Comparator; if ($compare->lessThan($this->version, $array['minimum_organizr_version'])) { $this->logger->warning('Minimum Organizr version needed: ' . $array['minimum_organizr_version']); $this->setResponse(500, 'Minimum Organizr version needed: ' . $array['minimum_organizr_version'] . ' | Current Version: ' . $this->version); return true; } // It is okay to user Plugin function - we should rename it so it is universal $files = $this->getPluginFilesFromRepo($theme, $array); if ($files) { $downloadList = $this->themeFileListFormat($files, $array['project_folder']); } else { $this->logger->warning('File list failed for: ' . $array['github_folder']); $this->setAPIResponse('error', 'Could not get download list for theme', 409); return false; } if (!$downloadList) { $this->logger->warning('Setting download list failed for: ' . $array['github_folder']); $this->setAPIResponse('error', 'Could not get download list for theme', 409); return false; } foreach ($downloadList as $k => $v) { $file = array( 'from' => $v['githubPath'], 'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $v['path'] . $v['fileName']), 'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $v['path']) ); $this->makeDir($this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'themes'); if (!$this->downloadFileToPath($file['from'], $file['to'], $file['path'])) { $this->setLoggerChannel('Theme Marketplace'); $this->logger->warning('Downloaded File Failed for: ' . $v['githubPath']); $this->setAPIResponse('error', 'Theme download failed', 500); return false; } } $this->updateInstalledThemes('install', $theme, $array); $this->setAPIResponse('success', 'Theme installed', 200, $array); return true; } public function themeFileListFormat($files, $folder) { $filesList = false; foreach ($files as $k => $v) { if ($v['type'] !== 'dir') { $filesList[] = array( 'fileName' => $v['name'], 'path' => $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . str_replace($v['name'], '', $v['path']), 'githubPath' => $v['download_url'] ); } } $this->makeDir($this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $folder); return $filesList; } public function pluginFileListFormat($files, $folder) { $filesList = false; foreach ($files as $k => $v) { if ($v['type'] !== 'dir') { $filesList[] = array( 'fileName' => $v['name'], 'path' => $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . str_replace($v['name'], '', $v['path']), 'githubPath' => $v['download_url'] ); } } $this->makeDir($this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $folder); return $filesList; } public function getPluginFilesFromGithub($plugin = 'test') { $url = 'https://api.github.com/repos/causefx/organizr/contents/' . $plugin . '?ref=v2-plugins'; $options = array('verify' => false); $response = Requests::get($url, array(), $options); if ($response->success) { return json_decode($response->body, true); } return false; } public function getBranchFromGithub($repo) { $url = 'https://api.github.com/repos/' . $repo; $options = array('verify' => false); $response = Requests::get($url, $this->setGithubAccessToken(), $options); try { if ($response->success) { $github = json_decode($response->body, true); return $github['default_branch'] ?? null; } else { $this->setLoggerChannel('Plugins'); $this->logger->warning('Plugin failed to get branch from Github', $this->apiResponseFormatter($response->body)); return false; } } catch (Requests_Exception $e) { $this->logger->error($e); $this->setAPIResponse('error', $e->getMessage(), 401); return false; } } public function getFilesFromGithub($repo, $branch) { if (!$repo || !$branch) { return false; } $url = 'https://api.github.com/repos/' . $repo . '/git/trees/' . $branch . '?recursive=1'; $options = array('verify' => false); $response = Requests::get($url, $this->setGithubAccessToken(), $options); try { if ($response->success) { $github = json_decode($response->body, true); return is_array($github) ? $github : null; } else { $this->setLoggerChannel('Plugins'); $this->logger->warning('Plugin failed to get branch from Github', $this->apiResponseFormatter($response->body)); return false; } } catch (Requests_Exception $e) { $this->logger->error($e); $this->setAPIResponse('error', $e->getMessage(), 401); return false; } } public function formatFilesFromGithub($files, $repo, $branch, $folder) { if (!$files || !$repo || !$branch || !$folder) { return false; } if (isset($files['tree'])) { $fileList = []; foreach ($files['tree'] as $k => $v) { if ($v['type'] !== 'tree') { $fileInfo = pathinfo($v['path']); $v['name'] = $fileInfo['basename']; $v['download_url'] = 'https://raw.githubusercontent.com/' . $repo . '/' . $branch . '/' . $v['path']; if ($folder == 'root') { $fileList[] = $v; } else { if (stripos($v['path'], $folder) !== false) { $v['path'] = (substr($v['path'], 0, strlen($folder)) == $folder) ? substr($v['path'], (strlen($folder) + 1)) : $v['path']; $fileList[] = $v; } } } } return $fileList; } return false; } public function getPluginFilesFromRepo($plugin, $pluginDetails) { if (stripos($pluginDetails['repo'], 'github.com') !== false) { $repo = explode('https://github.com/', $pluginDetails['repo']); } else { return false; } $branch = $this->getBranchFromGithub($repo[1]); if ($branch) { return $this->formatFilesFromGithub($this->getFilesFromGithub($repo[1], $branch), $repo[1], $branch, $pluginDetails['github_folder']); } return false; } public function installPlugin($plugin) { $this->setLoggerChannel('Plugin Marketplace'); $plugin = $this->reverseCleanClassName($plugin); $array = $this->getPluginsMarketplace(); $arrayLower = array_change_key_case($array); if (!$array) { $this->setAPIResponse('error', 'Could not access plugin marketplace', 409); return false; } if (!$arrayLower[$plugin]) { $this->setAPIResponse('error', 'Plugin does not exist in marketplace', 404); return false; } else { $key = array_search($plugin, array_keys($arrayLower)); $plugin = array_keys($array)[$key]; } $array = $array[$plugin]; // Check Version of Organizr against minimum version needed $compare = new Composer\Semver\Comparator; if ($compare->lessThan($this->version, $array['minimum_organizr_version'])) { $this->logger->warning('Minimum Organizr version needed: ' . $array['minimum_organizr_version']); $this->setResponse(500, 'Minimum Organizr version needed: ' . $array['minimum_organizr_version'] . ' | Current Version: ' . $this->version); return true; } $files = $this->getPluginFilesFromRepo($plugin, $array); if ($files) { $downloadList = $this->pluginFileListFormat($files, $array['project_folder']); } else { $this->logger->warning('File list failed for: ' . $array['github_folder']); $this->setAPIResponse('error', 'Could not get download list for plugin', 409); return false; } if (!$downloadList) { $this->logger->warning('Setting download list failed for: ' . $array['github_folder']); $this->setAPIResponse('error', 'Could not get download list for plugin', 409); return false; } foreach ($downloadList as $k => $v) { $file = array( 'from' => $v['githubPath'], 'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $v['path'] . $v['fileName']), 'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $v['path']) ); $this->makeDir($this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins'); if (!$this->downloadFileToPath($file['from'], $file['to'], $file['path'])) { $this->setLoggerChannel('Plugin Marketplace'); $this->logger->warning('Downloaded File Failed for: ' . $v['githubPath']); $this->setAPIResponse('error', 'Plugin download failed', 500); return false; } } $this->updateInstalledPlugins('install', $plugin, $array); $this->setAPIResponse('success', 'Plugin installed', 200, $array); return true; } public function removePlugin($plugin) { $this->setLoggerChannel('Plugin Marketplace'); $plugin = $this->reverseCleanClassName($plugin); $array = $this->getPluginsMarketplace(); $arrayLower = array_change_key_case($array); if (!$array) { $this->setAPIResponse('error', 'Could not access plugin marketplace', 409); return false; } if (!$arrayLower[$plugin]) { $this->setAPIResponse('error', 'Plugin does not exist in marketplace', 404); return false; } else { $key = array_search($plugin, array_keys($arrayLower)); $plugin = array_keys($array)[$key]; } $array = $array[$plugin]; $pluginDir = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $array['project_folder'] . DIRECTORY_SEPARATOR; $dirExists = file_exists($pluginDir); if ($dirExists) { if (!$this->rrmdir($pluginDir)) { $this->logger->info('Remove File Failed for: ' . $array['project_folder']); return false; } } else { $this->setAPIResponse('error', 'Plugin is not installed', 404); return false; } $this->updateInstalledPlugins('uninstall', $plugin, $array); $this->setAPIResponse('success', 'Plugin removed', 200, $array); return true; } public function updateInstalledPlugins($action, $plugin, $pluginDetails) { if (!$action || !$plugin || !$pluginDetails) { return false; } $config = $this->config['installedPlugins']; $config = is_array($config) ? $config : []; switch ($action) { case 'install': case 'update': $update[$plugin] = [ 'name' => $plugin, 'version' => $pluginDetails['version'], 'repo' => $pluginDetails['repo'] ]; $config = array_merge($config, $update); break; default: unset($config[$plugin]); break; } $this->updateConfig(['installedPlugins' => $config]); } public function updateInstalledThemes($action, $theme, $themeDetails) { if (!$action || !$theme || !$themeDetails) { return false; } $config = $this->config['installedThemes']; $config = is_array($config) ? $config : []; switch ($action) { case 'install': case 'update': $update[$theme] = [ 'name' => $theme, 'version' => $themeDetails['version'], 'repo' => $themeDetails['repo'], 'path' => 'data/themes/' . $themeDetails['project_folder'] ]; $config = array_merge($config, $update); break; default: unset($config[$theme]); break; } $this->updateConfig(['installedThemes' => $config]); } public function getThemesGithub() { $url = 'https://raw.githubusercontent.com/causefx/Organizr/v2-themes/themes.json'; $options = ($this->localURL($url)) ? array('verify' => false) : array(); $response = Requests::get($url, array(), $options); if ($response->success) { return json_decode($response->body, true); } return false; } public function getPluginsGithub() { $url = 'https://raw.githubusercontent.com/causefx/Organizr/v2-plugins/plugins.json'; $options = ($this->localURL($url)) ? array('verify' => false) : array(); try { $response = Requests::get($url, array(), $options); if ($response->success) { return json_decode($response->body, true); } } catch (Requests_Exception $e) { return false; } return false; } public function getPluginsMarketplace() { $plugins = $this->getPluginsGithubCombined(); foreach ($plugins as $pluginName => $pluginDetails) { $plugins[$pluginName]['installed'] = (isset($this->config['installedPlugins'][$pluginName])); $plugins[$pluginName]['installed_version'] = $this->config['installedPlugins'][$pluginName]['version'] ?? null; $plugins[$pluginName]['needs_update'] = ($plugins[$pluginName]['installed'] && ($plugins[$pluginName]['installed_version'] !== $plugins[$pluginName]['version'])); $plugins[$pluginName]['status'] = $this->getPluginStatus($plugins[$pluginName]); } return $plugins; } public function getThemesMarketplace() { $themes = $this->getThemesGithubCombined(); foreach ($themes as $themeName => $themeDetails) { $themes[$themeName]['installed'] = (isset($this->config['installedThemes'][$themeName])); $themes[$themeName]['installed_version'] = $this->config['installedThemes'][$themeName]['version'] ?? null; $themes[$themeName]['needs_update'] = ($themes[$themeName]['installed'] && ($themes[$themeName]['installed_version'] !== $themes[$themeName]['version'])); $themes[$themeName]['status'] = $this->getPluginStatus($themes[$themeName]); } return $themes; } public function getThemesGithubCombined() { // Organizr Repo $urls = [$this->getMarketplaceJSONFromRepo('https://github.com/Organizr/Organizr-Themes')]; foreach (explode(',', $this->config['externalThemeMarketplaceRepos']) as $repo) { $urls[] = $this->getMarketplaceJSONFromRepo($repo); } $themes = []; foreach ($urls as $repo) { $options = ($this->localURL($repo)) ? array('verify' => false) : array(); try { $response = Requests::get($repo, array(), $options); if ($response->success) { $themes = array_merge($themes, json_decode($response->body, true)); } else { $this->setLoggerChannel('Themes'); $this->logger->warning('Getting Marketplace items from Github', $this->apiResponseFormatter($response->body)); return false; } } catch (Requests_Exception $e) { //return false; } } return $themes; } public function getPluginStatus($pluginDetails) { if ($pluginDetails['needs_update']) { return 'Update Available'; } elseif ($pluginDetails['installed']) { return 'Up to date'; } else { return 'Not Installed'; } } public function getPluginsGithubCombined() { // Organizr Repo $urls = [$this->getMarketplaceJSONFromRepo('https://github.com/Organizr/Organizr-Plugins')]; foreach (explode(',', $this->config['externalPluginMarketplaceRepos']) as $repo) { $urls[] = $this->getMarketplaceJSONFromRepo($repo); } $plugins = []; foreach ($urls as $repo) { $options = ($this->localURL($repo)) ? array('verify' => false) : array(); try { $response = Requests::get($repo, array(), $options); if ($response->success) { $plugins = array_merge($plugins, json_decode($response->body, true)); } else { $this->setLoggerChannel('Plugins'); $this->logger->warning('Getting Marketplace items from Github', $this->apiResponseFormatter($response->body)); return false; } } catch (Requests_Exception $e) { //return false; } } return $plugins; } public function getMarketplaceJSONFromRepo($url) { if (stripos($url, '.json') !== false) { return $url; } elseif (stripos($url, 'github.com') !== false) { $repo = explode('https://github.com/', $url); $newURL = 'https://api.github.com/repos/' . $repo[1] . '/contents'; $options = ($this->localURL($newURL)) ? array('verify' => false) : array(); try { $response = Requests::get($newURL, $this->setGithubAccessToken(), $options); if ($response->success) { $jsonFiles = json_decode($response->body, true); foreach ($jsonFiles as $file) { if (stripos($file['name'], '.json') !== false) { return $file['download_url']; } } return false; } else { $this->setLoggerChannel('Plugins'); $this->logger->warning('Getting Marketplace JSON from Github', $this->apiResponseFormatter($response->body)); return false; } } catch (Requests_Exception $e) { return false; } } return false; } public function setGithubAccessToken() { return ($this->config['githubAccessToken'] !== '') ? ['Authorization' => 'token ' . $this->config['githubAccessToken']] : []; } public function formatGithubAccessToken() { $accessToken = $this->setGithubAccessToken(); if (count($accessToken) >= 1) { return key($accessToken) . ': ' . $accessToken[key($accessToken)]; } else { return ''; } } public function getOpenCollectiveBackers() { $url = 'https://opencollective.com/organizr/members/users.json?limit=100&offset=0'; $options = ($this->localURL($url)) ? array('verify' => false) : array(); try { $response = Requests::get($url, array(), $options); if ($response->success) { $api = json_decode($response->body, true); foreach ($api as $k => $backer) { $api[$k] = array_merge($api[$k], ['sortName' => strtolower($backer['name'])]); } $this->setAPIResponse('success', '', 200, $api); return $api; } } catch (Requests_Exception $e) { $this->setResponse(500, $e->getMessage()); return false; } $this->setAPIResponse('error', 'Error connecting to Open Collective', 409); return false; } public function getGithubSponsors() { $url = 'https://github.com/sponsors/causefx'; $options = ($this->localURL($url)) ? array('verify' => false) : array(); $response = Requests::get($url, array(), $options); if ($response->success) { $sponsors = []; $dom = new PHPHtmlParser\Dom; try { $dom->loadStr($response->body); $contents = $dom->find('div#sponsors a'); foreach ($contents as $content) { $html = $content->innerHtml; preg_match('/(@[a-zA-Z])\w+/', $html, $username); preg_match('/(?i)\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'\".,<>?«»""\'\']))/', $html, $image); if (isset($image[0]) && isset($username[0])) { $sponsors[] = [ 'name' => str_replace('@', '', $username[0]), 'sortName' => str_replace('@', '', strtolower($username[0])), 'image' => str_replace('s=60', 's=200', $image[0]), 'isActive' => true, 'type' => 'USER', 'role' => 'BACKER' ]; } } $this->setAPIResponse('success', '', 200, $sponsors); return $sponsors; } catch (\PHPHtmlParser\Exceptions\ChildNotFoundException|\PHPHtmlParser\Exceptions\CircularException|\PHPHtmlParser\Exceptions\LogicalException|\PHPHtmlParser\Exceptions\StrictException|\PHPHtmlParser\Exceptions\ContentLengthException|\PHPHtmlParser\Exceptions\NotLoadedException $e) { $this->setAPIResponse('error', 'Error connecting to Github', 409); return false; } } $this->setAPIResponse('error', 'Error connecting to Github', 409); return false; } public function getAllSponsors() { $sponsors = []; $list = [ 'openCollective' => $this->getOpenCollectiveBackers(), 'github' => $this->getGithubSponsors() ]; foreach ($list as $k => $sponsor) { if ($sponsor) { $sponsors = array_merge($sponsor, $sponsors); } } if ($sponsors) { usort($sponsors, function ($a, $b) { return $a['sortName'] <=> $b['sortName']; }); } $this->setAPIResponse('success', '', 200, $sponsors); return $sponsors; } public function getOrganizrSmtpFromAPI() { $url = 'https://api.organizr.app/?cmd=smtp'; $options = ($this->localURL($url)) ? array('verify' => false) : array(); try { $response = Requests::get($url, array(), $options); if ($response->success) { return json_decode($response->body, true); } } catch (Requests_Exception $e) { $this->setResponse(500, $e->getMessage()); return false; } return false; } public function saveOrganizrSmtpFromAPI() { $api = $this->getOrganizrSmtpFromAPI(); if ($api) { $this->updateConfigItems($api['response']['data']); $this->setAPIResponse(null, 'SMTP activated with Organizr SMTP account'); return true; } else { return false; } } public function guestHash($start, $end) { $ip = $this->userIP(); $ip = md5($ip); return substr($ip, $start, $end); } public function rrmdir($dir) { ini_set('max_execution_time', 0); set_time_limit(0); if (is_dir($dir)) { $files = scandir($dir); foreach ($files as $file) { if ($file != "." && $file != "..") { $this->rrmdir("$dir/$file"); } } rmdir($dir); } elseif (file_exists($dir)) { unlink($dir); } return true; } public function rcopy($src, $dst) { ini_set('max_execution_time', 0); set_time_limit(0); $src = $this->cleanPath($src); $dst = $this->cleanPath($dst); if (is_dir($src)) { if (!file_exists($dst)) : mkdir($dst); endif; $files = scandir($src); foreach ($files as $file) { if ($file != "." && $file != "..") { $this->rcopy("$src/$file", "$dst/$file"); } } } elseif (file_exists($src)) { copy($src, $dst); } return true; } public function unzipFile($zipFile) { ini_set('max_execution_time', 0); set_time_limit(0); $zip = new ZipArchive; $extractPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade/"; $this->setLoggerChannel('File Management'); if ($zip->open($extractPath . $zipFile) != "true") { $this->logger->warning('organizr could not unzip upgrade.zip'); } else { $this->logger->debug('organizr unzipped upgrade.zip'); } /* Extract Zip File */ $zip->extractTo($extractPath); $zip->close(); return true; } public function downloadFile($url, $path) { ini_set('max_execution_time', 0); set_time_limit(0); $folderPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade" . DIRECTORY_SEPARATOR; $this->setLoggerChannel('File Management'); if (!file_exists($folderPath)) { if (@!mkdir($folderPath)) { $this->logger->warning('Folder Creation failed'); return false; } } $newfname = $folderPath . $path; $context = stream_context_create( array( 'ssl' => array( 'verify_peer' => true, 'cafile' => $this->getCert() ) ) ); $file = fopen($url, 'rb', false, $context); if ($file) { $newf = fopen($newfname, 'wb'); if ($newf) { while (!feof($file)) { fwrite($newf, fread($file, 1024 * 8), 1024 * 8); } } } else { $this->logger->warning('Organizr could not download ' . $url); return false; } if ($file) { fclose($file); $this->logger->debug('Organizr finished downloading the github zip file'); } else { $this->logger->warning('Organizr could not download the github zip file'); return false; } if ($newf) { fclose($newf); $this->logger->debug('Organizr created upgrade zip file from github zip file'); } else { $this->logger->warning('Organizr could not create upgrade zip file from github zip file'); return false; } return true; } public function downloadFileToPath($from, $to, $path) { if (((stripos($from, 'api.github.com') !== false) || (stripos($from, 'raw.githubusercontent.com') !== false)) && $this->config['githubAccessToken'] !== '') { $context = stream_context_create( array( 'ssl' => array( 'verify_peer' => false, 'cafile' => $this->getCert() ), 'http' => array( 'method' => 'GET', 'header' => $this->formatGithubAccessToken() ) ) ); } else { $context = stream_context_create([]); } ini_set('max_execution_time', 0); set_time_limit(0); $this->makeDir($path); $file = fopen($from, 'rb', false, $context); if ($file) { $newf = fopen($to, 'wb', false, $context); if ($newf) { while (!feof($file)) { fwrite($newf, fread($file, 1024 * 8), 1024 * 8); } } } else { $this->logger->warning('Organizr could not download file'); } if ($file) { fclose($file); $this->logger->debug('Organizr finished downloading the file'); } else { $this->logger->warning('Organizr could not download file'); } if ($newf) { fclose($newf); $this->logger->debug('Organizr saved and/or moved the file'); } else { $this->logger->warning('Organizr could not save and/or move the file'); } return true; } public function getAllUsers($includeGroups = false) { $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM users' ), 'key' => 'users' ), ]; $groups = array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM groups ORDER BY group_id ASC' ), 'key' => 'groups' ); $addGroups = (isset($_GET['includeGroups']) || $includeGroups) ?? false; if ($addGroups) { array_push($response, $groups); } return $this->processQueries($response); } public function getAllGroups() { $response = [ array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM groups ORDER BY group_id ASC' ), 'key' => 'groups' ), ]; $users = array( 'function' => 'fetchAll', 'query' => array( 'SELECT * FROM users' ), 'key' => 'users' ); $addUsers = (isset($_GET['includeUsers'])) ?? false; if ($addUsers) { array_push($response, $users); } return $this->processQueries($response); } public function importUsers($array) { $imported = 0; if ($array) { foreach ($array as $user) { $password = $this->random_ascii_string(30); if ($user['username'] !== '' && $user['email'] !== '' && $password !== '') { $newUser = $this->createUser($user['username'], $password, $user['email']); if (!$newUser) { $this->setLoggerChannel('User Management'); $this->logger->warning('An error occurred during user import'); } else { $imported++; } } } } $this->setAPIResponse('success', 'Imported ' . $imported . ' users', 200); return true; } public function importUsersType($type) { if ($type !== '') { switch ($type) { case 'plex': return $this->importUsers($this->allPlexUsers(true)); case 'jellyfin': return $this->importUsers($this->allJellyfinUsers(true)); case 'emby': return $this->importUsers($this->allEmbyUsers(true)); default: return false; } } return false; } public function allPlexUsers($newOnly = false, $friendsOnly = false) { try { if (!empty($this->config['plexToken'])) { $url = 'https://plex.tv/api/users'; $headers = array( 'X-Plex-Token' => $this->config['plexToken'], ); $response = Requests::get($url, $headers); if ($response->success) { libxml_use_internal_errors(true); $userXML = simplexml_load_string($response->body); if (is_array($userXML) || is_object($userXML)) { $results = array(); foreach ($userXML as $child) { if (((string)$child['restricted'] == '0')) { if ($newOnly) { $taken = $this->usernameTaken((string)$child['username'], (string)$child['email']); if (!$taken) { $results[] = array( 'username' => (string)$child['username'], 'email' => (string)$child['email'], 'id' => (string)$child['id'], ); } } elseif ($friendsOnly) { $machineMatches = false; foreach ($child->Server as $server) { if ((string)$server['machineIdentifier'] == $this->config['plexID']) { $machineMatches = true; $shareId = $server['id']; } } if ($machineMatches) { $results[] = array( 'username' => (string)$child['username'], 'email' => (string)$child['email'], 'id' => (string)$child['id'], 'shareId' => (string)$shareId ); } } else { $results[] = array( 'username' => (string)$child['username'], 'email' => (string)$child['email'], 'id' => (string)$child['id'], ); } } } return $results; } } } return false; } catch (Requests_Exception $e) { $this->setLoggerChannel('User Management'); $this->logger->error($e); } return false; } public function allJellyfinUsers($newOnly = false) { try { if (!empty($this->config['jellyfinURL']) && !empty($this->config['jellyfinToken'])) { $url = $this->qualifyURL($this->config['jellyfinURL']) . '/Users?api_key=' . $this->config['jellyfinToken']; $headers = array(); $response = Requests::get($url, $headers); if ($response->success) { $users = json_decode($response->body, true); if (is_array($users) || is_object($users)) { $results = array(); foreach ($users as $child) { // Jellyfin doesn't list emails for some reason $email = $this->random_ascii_string(10) . '@placeholder.eml'; if ($newOnly) { $taken = $this->usernameTaken((string)$child['Name'], $email); if (!$taken) { $results[] = array( 'username' => (string)$child['Name'], 'email' => $email ); } } else { $results[] = array( 'username' => (string)$child['Name'], 'email' => $email, ); } } return $results; } } } return false; } catch (Requests_Exception $e) { $this->setLoggerChannel('User Management'); $this->logger->error($e); } return false; } public function allEmbyUsers($newOnly = false) { try { if (!empty($this->config['embyURL']) && !empty($this->config['embyToken'])) { $url = $this->qualifyURL($this->config['embyURL']) . '/Users?api_key=' . $this->config['embyToken']; $headers = array(); $response = Requests::get($url, $headers); if ($response->success) { $users = json_decode($response->body, true); if (is_array($users) || is_object($users)) { $results = array(); foreach ($users as $child) { // Emby doesn't list emails for some reason $email = $this->random_ascii_string(10) . '@placeholder.eml'; if ($newOnly) { $taken = $this->usernameTaken((string)$child['Name'], $email); if (!$taken) { $results[] = array( 'username' => (string)$child['Name'], 'email' => $email ); } } else { $results[] = array( 'username' => (string)$child['Name'], 'email' => $email, ); } } return $results; } } } return false; } catch (Requests_Exception $e) { $this->setLoggerChannel('User Management'); $this->logger->error($e); } return false; } public function validateEmail($email) { return filter_var(trim($email), FILTER_VALIDATE_EMAIL); } public function sanitizeEmail($email) { return filter_var(trim($email), FILTER_SANITIZE_EMAIL); } public function sanitizeUserString($string) { return htmlspecialchars(trim($string)); } public function updateUser($id, $array) { if (!$id) { $this->setAPIResponse('error', 'Id was not supplied', 422); return false; } if ((int)$id !== $this->user['userID']) { if (!$this->qualifyRequest('1', true)) { return false; } } $user = $this->getUserById($id); if ($user) { $array = $this->checkKeys($user, $array); } else { $this->setAPIResponse('error', 'User was not found', 404); return false; } if ($user['group_id'] == 0 && $this->user['groupID'] !== 0) { $this->setAPIResponse('error', 'Cannot update admin unless you are admin', 401); return false; } if (array_key_exists('username', $array)) { if ($array['username'] == '') { $this->setAPIResponse('error', 'Username was set but empty', 409); return false; } $array['username'] = $this->sanitizeUserString($array['username']); if ($this->usernameTaken($array['username'], $array['username'], $id)) { $this->setAPIResponse('error', 'Username: ' . $array['username'] . ' is already taken', 409); return false; } if (!$this->qualifyLength($array['username'], 50, true)) { return false; } } if (array_key_exists('email', $array)) { if ($array['email'] == '') { $this->setAPIResponse('error', 'Email was set but empty', 409); return false; } if ($this->validateEmail($array['email'])) { $array['email'] = $this->sanitizeEmail($array['email']); } else { $this->setResponse(409, 'Email is not a valid email', ['email' => $array['email']]); return false; } if ($this->usernameTaken($array['email'], $array['email'], $id)) { $this->setAPIResponse('error', 'Email: ' . $array['email'] . ' is already taken', 409); return false; } if (!$this->qualifyLength($array['email'], 50, true)) { return false; } } if (array_key_exists('group_id', $array)) { if ($array['group_id'] == '') { $array['group_id'] = 0; //$this->setAPIResponse('error', 'group_id was set but empty', 409); //return false; } if (!$this->qualifyRequest('1', false)) { $this->setAPIResponse('error', 'Cannot change your own group_id', 401); return false; } if (($id == $this->user['userID']) && $this->user['groupID'] == 0) { $array['group_id'] = 0; } if (($id == $this->user['userID']) && ($array['group_id'] == 0 && $this->user['groupID'] !== 0)) { $this->setAPIResponse('error', 'Only admins can make others admins', 401); return false; } $array['group'] = $this->getGroupByGroupId($array['group_id'])['group']; if (!$array['group']) { $this->setAPIResponse('error', 'group_id does not exist', 404); return false; } } if (array_key_exists('locked', $array)) { //$this->setAPIResponse('error', 'Cannot use endpoint to unlock or lock user - please use /users/{id}/lock', 409); //return false; } if (array_key_exists('password', $array)) { if ($array['password'] == '') { $this->setAPIResponse('error', 'Password was set but empty', 409); return false; } $array['password'] = password_hash($array['password'], PASSWORD_BCRYPT); } if (array_key_exists('image', $array)) { $array['image'] = $this->sanitizeUserString($array['image']); } if (array_key_exists('register_date', $array)) { $this->setAPIResponse('error', 'Cannot update register date', 409); return false; } $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE users SET', $array, 'WHERE id = ?', $id ) ), ]; $this->setAPIResponse(null, 'User info updated'); $this->setLoggerChannel('User Management'); $this->logger->info('Updated User Info for [' . $user['username'] . ']'); return $this->processQueries($response); } public function deleteUser($id) { $response = [ array( 'function' => 'query', 'query' => array( 'DELETE FROM users WHERE id = ?', $id ) ), ]; $userInfo = $this->getUserById($id); if ($id == $this->user['userID']) { $this->setAPIResponse('error', 'Cannot delete your own user', 409); return false; } if (!$userInfo) { $this->setAPIResponse('error', 'id not found', 404); return false; } $this->setLoggerChannel('User Management'); $this->logger->info('Deleted User [' . $userInfo['username'] . ']'); $this->revokeTokensByUserId($id); $this->setAPIResponse('success', 'User deleted', 204); return $this->processQueries($response); } public function addUser($array) { $username = $array['username'] ?? null; $password = $array['password'] ?? null; $email = $array['email'] ?? null; if (!$username) { $this->setAPIResponse('error', 'Username was not supplied', 409); return false; } if ($username == '') { $this->setResponse(409, 'Username was set but empty'); return false; } else { $username = $this->sanitizeUserString($username); } if (!$password) { $this->setAPIResponse('error', 'Password was not supplied', 409); return false; } if (!$email) { $this->setAPIResponse('error', 'Email was set not supplied', 409); return false; } if ($email == '') { $this->setAPIResponse('error', 'Email was set but empty', 409); return false; } if ($this->validateEmail($email)) { $email = $this->sanitizeEmail($email); } else { $this->setResponse(409, 'Email is not a valid email', ['email' => $email]); return false; } if (!$this->qualifyLength($username, 50, true)) { return false; } if (!$this->qualifyLength($email, 50, true)) { return false; } if (!$this->qualifyLength($password, 200, true)) { return false; } $this->setLoggerChannel('User Management'); if ($this->createUser($username, $password, $email)) { $this->logger->info('Account created for [' . $username . ']'); return true; } else { $this->logger->warning('An error occurred'); return false; } } public function createUser($username, $password, $email = null) { $username = $username ?? null; $password = $password ?? null; $email = ($email) ? $email : $this->random_ascii_string(10) . '@placeholder.eml'; if (!$username) { $this->setAPIResponse('error', 'Username was set but empty', 409); return false; } $username = $this->sanitizeUserString($username); if (!$password) { $this->setAPIResponse('error', 'Password was set but empty', 409); return false; } if ($email == '') { $this->setAPIResponse('error', 'Email was set but empty', 409); return false; } if ($this->validateEmail($email)) { $email = $this->sanitizeEmail($email); } else { $this->setResponse(409, 'Email is not a valid email', ['email' => $email]); return false; } if ($this->usernameTaken($username, $email)) { $this->setAPIResponse('error', 'Username: ' . $username . ' or Email: ' . $email . ' is already taken', 409); return false; } if (!$this->qualifyLength($username, 50, true)) { return false; } if (!$this->qualifyLength($email, 50, true)) { return false; } if (!$this->qualifyLength($password, 200, true)) { return false; } $defaults = $this->getDefaultGroup(); $userInfo = [ 'username' => $username, 'password' => password_hash($password, PASSWORD_BCRYPT), 'email' => $email, 'group' => $defaults['group'], 'group_id' => $defaults['group_id'], 'image' => $this->gravatar($email), 'register_date' => gmdate('Y-m-d H:i:s'), ]; $response = [ array( 'function' => 'query', 'query' => array( 'INSERT INTO [users]', $userInfo ) ), ]; $this->setAPIResponse('success', 'User created', 200); return $this->processQueries($response); } public function updateGroup($id, $array) { if (!$id || $id == '') { $this->setAPIResponse('error', 'id was not set', 422); return null; } if (!$array) { $this->setAPIResponse('error', 'no data was sent', 422); return null; } $groupInfo = $this->getGroupById($id); if ($groupInfo) { $array = $this->checkKeys($groupInfo, $array); } else { $this->setAPIResponse('error', 'No category info found', 404); return false; } if (array_key_exists('group_id', $array)) { $this->setAPIResponse('error', 'Cannot change group_id', 409); return false; } if (array_key_exists('group', $array)) { if ($array['group'] == '') { $this->setAPIResponse('error', 'Group was set but empty', 409); return false; } $array['group'] = $this->sanitizeUserString($array['group']); if ($this->isGroupNameTaken($array['group'], $id)) { $this->setAPIResponse('error', 'Group name: ' . $array['group'] . ' is already taken', 409); return false; } if (!$this->qualifyLength($array['group'], 50, true)) { return false; } } if (array_key_exists('image', $array)) { if ($array['image'] == '') { $this->setAPIResponse('error', 'Image was set but empty', 409); return false; } $array['image'] = $this->sanitizeUserString($array['image']); } if (array_key_exists('default', $array)) { if ($groupInfo['group_id'] == 0 || $groupInfo['group_id'] == 999) { $this->setAPIResponse('error', 'Setting ' . $groupInfo['group'] . ' as default group is not allowed', 409); return false; } if ($array['default']) { $this->clearGroupDefault(); $array['default'] = 1; } } $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE groups SET', $array, 'WHERE id = ?', $id ) ), ]; $this->setAPIResponse(null, 'Group info updated'); $this->setLoggerChannel('Group Management'); $this->logger->info('Edited Group Info for [' . $groupInfo['group'] . ']'); return $this->processQueries($response); } public function deleteGroup($id) { $response = [ array( 'function' => 'query', 'query' => array( 'DELETE FROM groups WHERE id = ?', $id ) ), ]; $groupInfo = $this->getGroupById($id); if ($groupInfo['group_id'] == 0 || $groupInfo['group_id'] == 999) { $this->setAPIResponse('error', 'Cannot delete group: ' . $groupInfo['group'] . ' as it is not allowed', 409); return false; } if ($this->getGroupUserCountById($id) >= 1) { $this->setAPIResponse('error', 'Cannot delete group as group still has users assigned to it', 409); return false; } if ($groupInfo) { $this->setLoggerChannel('Group Management'); $this->logger->info('Deleted Group [' . $groupInfo['group'] . ']'); $this->setAPIResponse('success', 'Group deleted', 204); return $this->processQueries($response); } else { $this->setAPIResponse('error', 'id not found', 404); return false; } } public function addGroup($array) { if (!$array) { $this->setAPIResponse('error', 'no data was sent', 422); return null; } $array = $this->checkKeys($this->getTableColumnsFormatted('groups'), $array); $array['default'] = ($array['default']) ?? 0; $array['group_id'] = $this->getNextGroupOrder() + 1; if (array_key_exists('group', $array)) { $array['group'] = $this->sanitizeUserString($array['group']); if ($this->isGroupNameTaken($array['group'])) { $this->setAPIResponse('error', 'Group name: ' . $array['group'] . ' is already taken', 409); return false; } if (!$this->qualifyLength($array['group'], 50, true)) { return false; } } else { $this->setAPIResponse('error', 'Group name was not supplied', 422); return false; } if (array_key_exists('image', $array)) { if ($array['image'] == '') { $this->setAPIResponse('error', 'Group image cannot be empty', 422); return false; } $array['image'] = $this->sanitizeUserString($array['image']); } else { $this->setAPIResponse('error', 'Group image was not supplied', 422); return false; } $response = [ array( 'function' => 'query', 'query' => array( 'INSERT INTO [groups]', $array ) ), ]; $this->setAPIResponse(null, 'Group added'); $this->setLoggerChannel('Group Management'); $this->logger->info('Added Group for [' . $array['group'] . ']'); return $this->processQueries($response); } public function userList($type = null) { switch ($type) { case 'plex': if (!empty($this->config['plexToken']) && !empty($this->config['plexID'])) { $url = 'https://plex.tv/api/servers/' . $this->config['plexID'] . '/shared_servers'; try { $headers = array( "Accept" => "application/json", "X-Plex-Token" => $this->config['plexToken'] ); $response = Requests::get($url, $headers, array()); libxml_use_internal_errors(true); if ($response->success) { $libraryList = array(); $plex = simplexml_load_string($response->body); foreach ($plex->SharedServer as $child) { if (!empty($child['username'])) { $username = (string)strtolower($child['username']); $email = (string)strtolower($child['email']); $libraryList['users'][$username] = (string)$child['id']; $libraryList['emails'][$email] = (string)$child['id']; $libraryList['both'][$username] = $email; } } $libraryList = array_change_key_case($libraryList, CASE_LOWER); return $libraryList; } } catch (Requests_Exception $e) { $this->setLoggerChannel('User Management'); $this->logger->error($e); } } break; default: # code... break; } return false; } public function encrypt($password, $key = null) { $key = ($key) ? $key : ((isset($this->config['organizrHash'])) ? $this->config['organizrHash'] : null); return openssl_encrypt($password, 'AES-256-CBC', $key, 0, $this->fillString($key, 16)); } public function decrypt($password, $key = null) { if (empty($password)) { return ''; } $key = ($key) ? $key : ((isset($this->config['organizrHash'])) ? $this->config['organizrHash'] : null); return openssl_decrypt($password, 'AES-256-CBC', $key, 0, $this->fillString($key, 16)); } public function checkValidCert($file) { if (file_exists($file)) { return filesize($file) > 0; } else { return false; } } public function getCert() { $url = 'http://curl.haxx.se/ca/cacert.pem'; $file = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'functions' . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'cacert.pem'; $file2 = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'functions' . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'cacert-initial.pem'; $useCert = ($this->checkValidCert($file)) ? $file : $file2; if ($this->config['selfSignedCert'] !== '') { if (file_exists($this->config['selfSignedCert'])) { return $this->config['selfSignedCert']; } } $context = stream_context_create( array( 'ssl' => array( 'verify_peer' => true, 'cafile' => $useCert ) ) ); if (!$this->checkValidCert($file) || (file_exists($file) && time() - 2592000 > filemtime($file))) { file_put_contents($file, fopen($url, 'r', false, $context)); } return ($this->checkValidCert($file)) ? $file : $file2; } public function hasCustomCert() { return file_exists($this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'custom.pem'); } public function getCustomCert() { return ($this->hasCustomCert()) ? $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'custom.pem' : false; } public function uploadCert() { $filesCheck = array_filter($_FILES); if (!empty($filesCheck) && $this->approvedFileExtension($_FILES['file']['name'], 'cert')) { ini_set('upload_max_filesize', '10M'); ini_set('post_max_size', '10M'); $tempFile = $_FILES['file']['tmp_name']; $targetPath = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR; $targetFile = $targetPath . 'custom.pem'; $this->setAPIResponse(null, pathinfo($_FILES['file']['name'], PATHINFO_BASENAME) . ' has been uploaded', null); $this->makeDir($this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cert'); return move_uploaded_file($tempFile, $targetFile); } else { $this->setAPIResponse('error', pathinfo($_FILES['file']['name'], PATHINFO_BASENAME) . ' is not approved to be uploaded', 403); return false; } } public function createCronFile() { $file = $this->root . DIRECTORY_SEPARATOR . 'Cron.txt'; file_put_contents($file, time()); } public function checkCronFile() { $file = $this->root . DIRECTORY_SEPARATOR . 'Cron.txt'; return file_exists($file) && time() - 120 < filemtime($file); } public function plexJoinAPI($array) { $username = ($array['username']) ?? null; $email = ($array['email']) ?? null; $password = ($array['password']) ?? null; if (!$username) { $this->setAPIResponse('error', 'Username not supplied', 409); return false; } if (!$email) { $this->setAPIResponse('error', 'Email not supplied', 409); return false; } if (!$password) { $this->setAPIResponse('error', 'Password not supplied', 409); return false; } return $this->plexJoin($username, $email, $password); } public function plexJoin($username, $email, $password) { try { $url = 'https://plex.tv/api/v2/users'; $headers = array( 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded', 'X-Plex-Product' => 'Organizr', 'X-Plex-Version' => '2.0', 'X-Plex-Client-Identifier' => $this->config['uuid'], ); $data = array( 'email' => $email, 'username' => $username, 'password' => $password, ); $response = Requests::post($url, $headers, $data, array()); $json = json_decode($response->body, true); $errors = !empty($json['errors']); $success = empty($json['errors']); //Use This for later $errorMessage = ''; if ($errors) { foreach ($json['errors'] as $error) { if (isset($error['message']) && isset($error['field'])) { $errorMessage .= "[Plex.tv Error: " . $error['message'] . " for field: (" . $error['field'] . ")]"; } } } $msg = (!empty($success) && empty($errors)) ? 'User has joined Plex' : $errorMessage; $status = (!empty($success) && empty($errors)) ? 'success' : 'error'; $code = (!empty($success) && empty($errors)) ? 200 : 422; $this->setAPIResponse($status, $msg, $code); return (!empty($success) && empty($errors)); } catch (Requests_Exception $e) { $this->setLoggerChannel('User Management'); $this->logger->error($e); $this->setAPIResponse('error', 'An Error Occurred', 409); return false; } return false; } public function lockCurrentUser() { if ($this->user['userID'] == '999') { $this->setAPIResponse('error', 'Locking not allowed on Guest users', 409); return false; } return $this->lockUser($this->user['userID']); } public function lockUser($id) { $user = $this->getUserById($id); if (!$user) { $this->setAPIResponse('error', 'User not found', 404); return false; } $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE users SET', ['locked' => '1'], 'WHERE id = ?', $id ) ), ]; $this->setLoggerChannel('User Management'); $this->logger->info('User: ' . $user['username'] . ' account locked'); $this->setAPIResponse('success', 'User account locked', 200); return $this->processQueries($response); } public function unlockCurrentUser($array) { if ($array['password'] == '') { $this->setAPIResponse('error', 'Password Not Set', 422); return false; } $user = $this->getUserById($this->user['userID']); if (!password_verify($array['password'], $user['password'])) { $this->setAPIResponse('error', 'Password Incorrect', 401); return false; } return $this->unlockUser($this->user['userID']); } public function unlockUser($id) { $user = $this->getUserById($id); if (!$user) { $this->setAPIResponse('error', 'User not found', 404); return false; } $response = [ array( 'function' => 'query', 'query' => array( 'UPDATE users SET', ['locked' => '0'], 'WHERE id = ?', $id ) ), ]; $this->setLoggerChannel('User Management'); $this->logger->info('User: ' . $user['username'] . ' account unlocked'); $this->setAPIResponse('success', 'User account unlocked', 200); return $this->processQueries($response); } public function youtubeSearch($query) { if (!$query) { $this->setAPIResponse('error', 'No query supplied', 422); return false; } $keys = array( 'AIzaSyBsdt8nLJRMTwOq5PY5A5GLZ2q7scgn01w', 'AIzaSyD-8SHutB60GCcSM8q_Fle38rJUV7ujd8k', 'AIzaSyBzOpVBT6VII-b-8gWD0MOEosGg4hyhCsQ', 'AIzaSyBKnRe1P8fpfBHgooJpmT0WOsrdUtZ4cpk' ); $randomKeyIndex = array_rand($keys); $key = $keys[$randomKeyIndex]; $apikey = ($this->config['youtubeAPI'] !== '') ? $this->config['youtubeAPI'] : $key; $results = false; $url = "https://www.googleapis.com/youtube/v3/search?part=snippet&q=$query+official+trailer&part=snippet&maxResults=1&type=video&videoDuration=short&key=$apikey"; $response = Requests::get($url); if ($response->success) { $results = json_decode($response->body, true); $this->setAPIResponse('success', null, 200, $results); return $results; } else { $this->setAPIResponse('error', 'Bad response from YouTube', 500); return false; } } public function scrapePage($array) { try { $url = $array['url'] ?? false; $type = $array['type'] ?? false; if (!$url) { $this->setAPIResponse('error', 'URL was not supplied', 422); return false; } $url = $this->qualifyURL($url); $data = array( 'full_url' => $url, 'drill_url' => $this->qualifyURL($url, true) ); $options = array('verify' => false); $response = Requests::get($url, array(), $options); $data['response_code'] = $response->status_code; if ($response->success) { $data['result'] = 'Success'; switch ($type) { case 'html': $data['data'] = html_entity_decode($response->body); break; case 'json': $data['data'] = json_decode($response->body); break; default: $data['data'] = $response->body; } $this->setAPIResponse('success', null, 200, $data); return $data; } else { $this->setAPIResponse('error', 'Error getting successful response', 500); return false; } } catch (Requests_Exception $e) { $this->setResponse(500, $e->getMessage()); return false; } } public function chooseInstance($url = null, $token = null, $instance = 0, $type = null) { if (!$url || !$token) { return false; } $list = $this->csvHomepageUrlToken($url, $token); if ($type) { $type = strtolower($type); switch ($type) { case 'url': case 'token': break; default: $type = 'url'; break; } if (is_numeric($instance)) { return $list[$instance][$type]; } else { return $list; } } if (is_numeric($instance)) { return $list[$instance]; } else { return $list; } } public function CBPFWTabs() { return ' '; } public function socksHeadingHTML($app) { return '

' . ucwords($app) . ' SOCKS API Connection

Using this feature allows you to access the API without having to reverse proxy it. Just access it from:

If you are using multiple URL\'s (using the csv method) you will have to use the url like these:


'; } public function socksListing($app = null) { switch ($app) { case 'sonarr': $appDetails = [ 'url' => 'sonarrURL', 'enabled' => 'sonarrSocksEnabled', 'auth' => 'sonarrSocksAuth', 'header' => 'X-Api-Key' ]; break; case 'radarr': $appDetails = [ 'url' => 'radarrURL', 'enabled' => 'radarrSocksEnabled', 'auth' => 'radarrSocksAuth', 'header' => 'X-Api-Key' ]; break; case 'lidarr': $appDetails = [ 'url' => 'lidarrURL', 'enabled' => 'lidarrSocksEnabled', 'auth' => 'lidarrSocksAuth', 'header' => 'X-Api-Key' ]; break; case 'sabnzbd': $appDetails = [ 'url' => 'sabnzbdURL', 'enabled' => 'sabnzbdSocksEnabled', 'auth' => 'sabnzbdSocksAuth', 'header' => null ]; break; case 'nzbget': $appDetails = [ 'url' => 'nzbgetURL', 'enabled' => 'nzbgetSocksEnabled', 'auth' => 'nzbgetSocksAuth', 'header' => 'Authorization' ]; break; case 'tautulli': $appDetails = [ 'url' => 'tautulliURL', 'enabled' => 'tautulliSocksEnabled', 'auth' => 'tautulliSocksAuth', 'header' => null ]; break; case 'qbittorrent': $appDetails = [ 'url' => 'qBittorrentURL', 'enabled' => 'qBittorrentSocksEnabled', 'auth' => 'qBittorrentSocksAuth', 'header' => null ]; break; default: $appDetails = null; } return $appDetails; } public function socks($appDetails, $requestObject, $multiple = null) { $url = $appDetails['url']; $enabled = $appDetails['enabled']; $auth = $appDetails['auth']; $header = $appDetails['header']; $error = false; if (!$this->config[$enabled]) { $error = true; $this->setAPIResponse('error', 'SOCKS module is not enabled', 409); } if (!$this->qualifyRequest($this->config[$auth], true)) { $error = true; } if (strpos($this->config[$url], ',') !== false) { if (!$multiple) { $error = true; $this->setAPIResponse('error', 'Multiple URLs found in field, please use /api/v2/multiple/socks endpoint', 409); } } else { if ($multiple) { $error = true; $this->setAPIResponse('error', 'Multiple endpoint accessed but multiple URLs not found in field, please use /api/v2/socks endpoint', 409); } } if ($error) { return null; } if ($multiple) { $instance = $multiple - 1; $pre = explode('/api/v2/multiple/socks/', $requestObject->getUri()->getPath()); $pre[1] = $this->replace_first('/' . $multiple . '/', '/', $pre[1]); // sent url twice since we arent using tokens $list = $this->csvHomepageUrlToken($this->config[$url], $this->config[$url]); $appURL = $list[$instance]['url']; } else { $pre = explode('/api/v2/socks/', $requestObject->getUri()->getPath()); $appURL = $this->config[$url]; } $endpoint = explode('/', $pre[1]); $new = urldecode(preg_replace('/' . $endpoint[0] . '/', '', $pre[1], 1)); $getParams = ($_GET) ? '?' . http_build_query($_GET) : ''; $url = $this->qualifyURL($appURL) . $new . $getParams; $url = $this->cleanPath($url); $options = ($this->localURL($appURL)) ? ['verify' => false, 'timeout' => 120] : ['timeout' => 120]; $headers = []; $apiData = $this->apiData($requestObject, false); if ($header) { if ($requestObject->hasHeader($header)) { $headerKey = $requestObject->getHeaderLine($header); $headers[$header] = $headerKey; } } if ($requestObject->hasHeader('Content-Type')) { $headerKey = $requestObject->getHeaderLine('Content-Type'); $headers['Content-Type'] = $headerKey; } $debugInformation = [ 'type' => $requestObject->getMethod(), 'headerType' => $requestObject->getHeaderLine('Content-Type'), 'header' => $header, 'headers' => $headers, 'url' => $url, 'options' => $options, 'data' => $apiData, ]; $this->setLoggerChannel('Socks')->debug('Sending Socks request', $debugInformation); try { switch ($requestObject->getMethod()) { case 'GET': $call = Requests::get($url, $headers, $options); break; case 'POST': $call = Requests::post($url, $headers, $apiData, $options); break; case 'DELETE': $call = Requests::delete($url, $headers, $options); break; case 'PUT': $call = Requests::put($url, $headers, $apiData, $options); break; default: $call = Requests::get($url, $headers, $options); } if ($this->config['socksDebug']) { if ($this->json_validator($call->body)) { $logData = json_decode($call->body, true); $size = (!is_numeric($this->config['maxSocksDebugSize']) || $this->config['maxSocksDebugSize'] == 0) ? 1 : $this->config['maxSocksDebugSize']; if (count($logData) > $size) { $logData = 'Count too large to output'; } } else { $logData = $call->body; } } else { $logData = 'Debug not enabled'; } $this->setLoggerChannel('Socks')->debug('Socks Response', ['body' => $logData, 'debug' => $debugInformation]); return $call->body; } catch (Requests_Exception $e) { $this->setResponse(500, $e->getMessage()); $this->setLoggerChannel('Socks')->critical($e, $debugInformation); return null; } } public function getPlexServers() { if ($this->config['plexToken'] == '') { $this->setAPIResponse('error', 'Plex Token cannot be empty', 422); return false; } $ownedOnly = isset($_GET['owned']) ?? false; $url = $this->qualifyURL('https://plex.tv/pms/servers'); $options = ($this->localURL($url)) ? array('verify' => false) : array(); $headers = [ 'X-Plex-Product' => 'Organizr', 'X-Plex-Version' => '2.0', 'X-Plex-Client-Identifier' => '01010101-10101010', 'X-Plex-Token' => $this->config['plexToken'], ]; try { $response = Requests::get($url, $headers, $options); libxml_use_internal_errors(true); if ($response->success) { $items = array(); $plex = simplexml_load_string($response->body); foreach ($plex as $server) { if ($ownedOnly) { if ($server['owned'] == 1) { $items[] = array( 'name' => (string)$server['name'], 'address' => (string)$server['address'], 'machineIdentifier' => (string)$server['machineIdentifier'], 'owned' => (float)$server['owned'], ); } } else { $items[] = array( 'name' => (string)$server['name'], 'address' => (string)$server['address'], 'machineIdentifier' => (string)$server['machineIdentifier'], 'owned' => (float)$server['owned'], ); } } $this->setResponse(200, null, $items); return $items; } else { $message = $this->testAndFormatString($response->body); $this->setResponse(500, 'Plex Error occurred', $message['data']); $this->setLoggerChannel('Plex Connection')->warning('Plex Error', $message); return $message; } } catch (Requests_Exception $e) { $this->setLoggerChannel('Plex Connection')->error($e); $this->setResponse(500, $e->getMessage()); return false; } } public function getIcons() { $term = $_GET['search'] ?? null; $page = $_GET['page'] ?? 1; $limit = $_GET['limit'] ?? 20; $offset = ($page * $limit) - $limit; $goodIcons['results'] = []; $goodIcons['limit'] = $limit; $goodIcons['page'] = $page; $goodIcons['term'] = $term; $allIcons = file_get_contents($this->root . '/js/icons.json'); $iconListing = json_decode($allIcons, true); foreach ($iconListing as $setKey => $set) { foreach ($set['children'] as $k => $v) { if ($term) { if (stripos($v['text'], $term) !== false) { $goodIcons['results'][] = $v; } } else { $goodIcons['results'][] = $v; } } } $total = count($goodIcons['results']); $goodIcons['total'] = $total; $goodIcons['results'] = array_slice($goodIcons['results'], $offset, $limit); $goodIcons['pagination']['more'] = $page < (ceil($total / $limit)); return $goodIcons; } public function getJournalMode() { $response = [ array( 'function' => 'fetch', 'query' => 'PRAGMA journal_mode', ), ]; $query = $this->processQueries($response); if ($query) { if ($query['journal_mode']) { $this->setResponse(200, null, $query); } else { $this->setResponse(500, 'Error getting Journal Mode'); } } else { $this->setResponse(404, 'Journal Mode not found'); } return $query; } public function setJournalMode($option = 'WAL') { $option = strtoupper($option); switch ($option) { case 'WAL': case 'DELETE': break; default: return false; } $response = [ array( 'function' => 'fetch', 'query' => 'PRAGMA journal_mode = \'' . $option . '\';', ), ]; $query = $this->processQueries($response); if ($query) { if ($query['journal_mode']) { $this->setResponse(200, 'Journal Mode updated to: ' . $option, $query); } else { $this->setResponse(500, 'Error getting Journal Mode'); } } else { $this->setResponse(404, 'Journal Mode not found'); } return $query; } public function testCronSchedule($schedule = null) { if (is_array($schedule)) { $schedule = str_replace('_', ' ', array_keys($schedule)[0]); } if (!$schedule) { $this->setResponse(409, 'Schedule was not supplied'); return false; } try { $schedule = new Cron\CronExpression($schedule); $this->setResponse(200, 'Schedule was validated'); return true; } catch (InvalidArgumentException $e) { $this->setResponse(500, $e->getMessage()); return false; } } public function testFolder($folder = null) { $folder = $folder['folder'] ?? null; if (!$folder) { $this->setResponse(409, 'Folder was not supplied'); return false; } $testFolder = $this->makeDir($folder); if ($testFolder) { $this->setResponse(200, 'Folder approved for logs'); return true; } else { $this->setResponse(409, 'Folder path is not valid or permissions insufficient'); return false; } } public function replaceStringInDatabase($string) { $databaseStringList = [ 'AUTOINCREMENT' => [ 'sqlite3' => 'AUTOINCREMENT', 'mysqli' => 'AUTO_INCREMENT', 'postgre' => 'AUTOINCREMENT' ], 'COLLATE NOCASE' => [ 'sqlite3' => 'COLLATE NOCASE', 'mysqli' => '', 'postgre' => '' ], 'INTEGER PRIMARY KEY AUTOINCREMENT' => [ 'sqlite3' => 'INTEGER PRIMARY KEY AUTOINCREMENT', 'mysqli' => 'INTEGER PRIMARY KEY AUTOINCREMENT', 'postgre' => 'SERIAL PRIMARY KEY' ], 'DATETIME' => [ 'sqlite3' => 'DATETIME', 'mysqli' => 'DATETIME', 'postgre' => 'TIMESTAMP' ], ]; if (gettype($string) == 'string') { foreach ($databaseStringList as $item => $value) { if (stripos($string, $item) !== false) { $string = str_ireplace($item, $databaseStringList[$item][$this->config['driver']], $string); } } } return $string; } public function cleanDatabaseQuery($query) { if (is_array($query)) { foreach ($query as $key => $value) { $query[$key] = $this->cleanDatabaseQuery($value); } return $query; } else { return $this->replaceStringInDatabase($query); } } protected function processQueries(array $request, $migration = false) { $results = array(); $firstKey = ''; if ($this->config['includeDatabaseQueriesInDebug']) { $this->setLoggerChannel('Database'); $this->logger->debug('Query to database', $request); } foreach ($request as $k => $v) { try { $v['query'] = $this->cleanDatabaseQuery($v['query']); $query = ($migration) ? $this->otherDb->query($v['query']) : $this->db->query($v['query']); $keyName = (isset($v['key'])) ? $v['key'] : $k; $firstKey = (isset($v['key']) && $k == 0) ? $v['key'] : $k; switch ($v['function']) { case 'fetchAll': $results[$keyName] = $query->fetchAll(); break; case 'fetch': // PHP 8 Fix? $query->setRowClass(null); $results[$keyName] = $query->fetch(); break; case 'getAffectedRows': $results[$keyName] = $query->getAffectedRows(); break; case 'getRowCount': $results[$keyName] = $query->getRowCount(); break; case 'fetchSingle': // PHP 8 Fix? $query->setRowClass(null); $results[$keyName] = $query->fetchSingle(); break; case 'query': $results[$keyName] = $query; break; default: return false; } } catch (Exception $e) { $this->setLoggerChannel('Database'); $this->logger->critical($e, $v['query']); return false; } } if ($this->config['includeDatabaseQueriesInDebug']) { $this->logger->debug('Results from database', $results); } return count($request) > 1 ? $results : $results[$firstKey]; } }