'])
],
'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', '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('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 . '
']
)
],
];
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 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 = [
array(
'function' => 'query',
'query' => array(
'INSERT INTO [tokens]',
$addToken
)
),
];
$this->processQueries($response);
if ($token) {
$this->logger->debug('Token has been created');
} 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;
// 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');
} 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 ($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 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
],
'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'],
'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'],
],
'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 = '
';
$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('#sponsors .clearfix div');
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->setLoggerChannel('User Management');
$this->logger->info('Deleted User [' . $userInfo['username'] . ']');
$this->setAPIResponse('success', 'User deleted', 204);
return $this->processQueries($response);
} else {
$this->setAPIResponse('error', 'id not found', 404);
return false;
}
}
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: