'Invites', // Plugin Name
'author' => 'CauseFX', // Who wrote the plugin
'category' => 'Management', // One to Two Word Description
'link' => '', // Link to plugin info
'license' => 'personal', // License Type use , for multiple
'idPrefix' => 'INVITES', // html element id prefix
'configPrefix' => 'INVITES', // config file prefix for array items without the hyphen
'version' => '1.1.0', // SemVer of plugin
'image' => 'api/plugins/invites/logo.png', // 1:1 non transparent image for plugin
'settings' => true, // does plugin need a settings modal?
'bind' => true, // use default bind to make settings page - true or false
'api' => 'api/v2/plugins/invites/settings', // api route for settings page
'homepage' => false // Is plugin for use on homepage? true or false
);
class Invites extends Organizr
{
public function __construct()
{
parent::__construct();
$this->_pluginUpgradeCheck();
}
public function _pluginUpgradeCheck()
{
if ($this->hasDB()) {
$compare = new Composer\Semver\Comparator;
$oldVer = $this->config['INVITES-dbVersion'];
// Upgrade check start for version below
$versionCheck = '1.1.0';
if ($compare->lessThan($oldVer, $versionCheck)) {
$oldVer = $versionCheck;
$this->_pluginUpgradeToVersion($versionCheck);
}
// End Upgrade check start for version above
// Update config.php version if different to the installed version
if ($GLOBALS['plugins']['Invites']['version'] !== $this->config['INVITES-dbVersion']) {
$this->updateConfig(array('INVITES-dbVersion' => $oldVer));
$this->setLoggerChannel('Invites Plugin');
$this->logger->debug('Updated INVITES-dbVersion to ' . $oldVer);
}
return true;
}
}
public function _pluginUpgradeToVersion($version = '1.1.0')
{
switch ($version) {
case '1.1.0':
$this->_addInvitedByColumnToDatabase();
break;
}
$this->setResponse(200, 'Ran plugin update function for version: ' . $version);
return true;
}
public function _addInvitedByColumnToDatabase()
{
$addColumn = $this->addColumnToDatabase('invites', 'invitedby', 'TEXT');
$this->setLoggerChannel('Invites Plugin');
if ($addColumn) {
$this->logger->info('Updated Invites Database');
} else {
$this->logger->warning('Could not update Invites Database');
}
}
public function _invitesPluginGetCodes()
{
if ($this->qualifyRequest(1, false)) {
$response = [
array(
'function' => 'fetchAll',
'query' => 'SELECT * FROM invites'
)
];
} else {
$response = [
array(
'function' => 'fetchAll',
'query' => array(
'SELECT * FROM invites WHERE invitedby = ?',
$this->user['username']
)
)
];
}
return $this->processQueries($response);
}
public function _invitesPluginCreateCode($array)
{
$code = ($array['code']) ?? null;
$username = ($array['username']) ?? null;
$email = ($array['email']) ?? null;
$invites = $this->_invitesPluginGetCodes();
$inviteCount = count($invites);
if (!$this->qualifyRequest(1, false)) {
if ($this->config['INVITES-maximum-invites'] != 0 && $inviteCount >= $this->config['INVITES-maximum-invites']) {
$this->setAPIResponse('error', 'Maximum number of invites reached', 409);
return false;
}
}
if (!$code) {
$this->setAPIResponse('error', 'Code not supplied', 409);
return false;
}
if (!$username) {
$this->setAPIResponse('error', 'Username not supplied', 409);
return false;
}
if (!$email) {
$this->setAPIResponse('error', 'Email not supplied', 409);
return false;
}
$newCode = [
'code' => $code,
'email' => $email,
'username' => $username,
'valid' => 'Yes',
'type' => $this->config['INVITES-type-include'],
'invitedby' => $this->user['username'],
'date' => gmdate('Y-m-d H:i:s')
];
$response = [
array(
'function' => 'query',
'query' => array(
'INSERT INTO [invites]',
$newCode
)
)
];
$query = $this->processQueries($response);
if ($query) {
$this->setLoggerChannel('Invites')->info('Added Invite [' . $code . ']');
if ($this->config['PHPMAILER-enabled']) {
$PhpMailer = new PhpMailer();
$emailTemplate = array(
'type' => 'invite',
'body' => $this->config['PHPMAILER-emailTemplateInviteUser'],
'subject' => $this->config['PHPMAILER-emailTemplateInviteUserSubject'],
'user' => $username,
'password' => null,
'inviteCode' => $code,
);
$emailTemplate = $PhpMailer->_phpMailerPluginEmailTemplate($emailTemplate);
$sendEmail = array(
'to' => $email,
'subject' => $emailTemplate['subject'],
'body' => $PhpMailer->_phpMailerPluginBuildEmail($emailTemplate),
);
$PhpMailer->_phpMailerPluginSendEmail($sendEmail);
}
$this->setAPIResponse('success', 'Invite Code: ' . $code . ' has been created', 200);
return true;
} else {
return false;
}
}
public function _invitesPluginVerifyCode($code)
{
$response = [
array(
'function' => 'fetchAll',
'query' => array(
'SELECT * FROM invites WHERE valid = "Yes" AND code = ? COLLATE NOCASE',
$code
)
)
];
if ($this->processQueries($response)) {
$this->setAPIResponse('success', 'Code has been verified', 200);
return true;
} else {
$this->setAPIResponse('error', 'Code is invalid', 401);
return false;
}
}
public function _invitesPluginDeleteCode($code)
{
if ($this->qualifyRequest(1, false)) {
$response = [
array(
'function' => 'fetch',
'query' => array(
'SELECT * FROM invites WHERE code = ? COLLATE NOCASE',
$code
)
)
];
} else {
if ($this->config['INVITES-allow-delete']) {
$response = [
array(
'function' => 'fetch',
'query' => array(
'SELECT * FROM invites WHERE invitedby = ? AND code = ? COLLATE NOCASE',
$this->user['username'],
$code
)
)
];
} else {
$this->setAPIResponse('error', 'You are not permitted to delete invites.', 409);
return false;
}
}
$info = $this->processQueries($response);
if (!$info) {
$this->setAPIResponse('error', 'Code not found', 404);
return false;
}
$response = [
array(
'function' => 'query',
'query' => array(
'DELETE FROM invites WHERE code = ? COLLATE NOCASE',
$code
)
)
];
$this->setAPIResponse('success', 'Code has been deleted', 200);
return $this->processQueries($response);
}
public function _invitesPluginUseCode($code, $array)
{
$code = ($code) ?? null;
$mail = $this->_getEmailFronInviteCode($code);
$usedBy = ($array['usedby']) ?? null;
$now = date("Y-m-d H:i:s");
$currentIP = $this->userIP();
if ($this->_invitesPluginVerifyCode($code)) {
$updateCode = [
'valid' => 'No',
'usedby' => $usedBy,
'dateused' => $now,
'ip' => $currentIP
];
$response = [
array(
'function' => 'query',
'query' => array(
'UPDATE invites SET',
$updateCode,
'WHERE code=? COLLATE NOCASE',
$code
)
)
];
$query = $this->processQueries($response);
$this->setLoggerChannel('Invites')->info('Invite Used [' . $code . '] by ' . $usedBy);
return $this->_invitesPluginAction($usedBy, 'share', $this->config['INVITES-type-include'], $mail);
} else {
return false;
}
}
public function _invitesPluginLibraryList($type = null)
{
switch ($type) {
case 'plex':
if (!empty($this->config['plexToken']) && !empty($this->config['plexID'])) {
$url = 'https://plex.tv/api/servers/' . $this->config['plexID'];
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->Server->Section as $child) {
$libraryList['libraries'][(string)$child['title']] = (string)$child['id'];
}
if ($this->config['INVITES-plexLibraries'] !== '') {
$noLongerId = 0;
$libraries = explode(',', $this->config['INVITES-plexLibraries']);
foreach ($libraries as $child) {
if (!$this->search_for_value($child, $libraryList)) {
$libraryList['libraries']['No Longer Exists - ' . $noLongerId] = $child;
$noLongerId++;
}
}
}
$libraryList = array_change_key_case($libraryList, CASE_LOWER);
return $libraryList;
}
} catch (Requests_Exception $e) {
$this->setLoggerChannel('Plex')->error($e);
return false;
};
}
break;
default:
# code...
break;
}
return false;
}
public function _invitesPluginGetSettings()
{
if ($this->config['plexID'] !== '' && $this->config['plexToken'] !== '' && $this->config['INVITES-type-include'] == 'plex') {
$loop = $this->_invitesPluginLibraryList($this->config['INVITES-type-include'])['libraries'];
foreach ($loop as $key => $value) {
$libraryList[] = array(
'name' => $key,
'value' => $value
);
}
} else {
$libraryList = array(
array(
'name' => 'Refresh page to update List',
'value' => '',
'disabled' => true,
),
);
}
$komgaRoles = $this->_getKomgaRoles();
$komgalibrary = $this->_getKomgaLibraries();
$nextcloudRoles = $this->_getNextcloudGroups();
return array(
'Backend' => array(
array(
'type' => 'select',
'name' => 'INVITES-type-include',
'label' => 'Media Server',
'value' => $this->config['INVITES-type-include'],
'options' => array(
array(
'name' => 'N/A',
'value' => 'n/a'
),
array(
'name' => 'Plex',
'value' => 'plex'
),
array(
'name' => 'Emby',
'value' => 'emby'
)
)
),
array(
'type' => 'select',
'name' => 'INVITES-Auth-include',
'label' => 'Minimum Authentication',
'value' => $this->config['INVITES-Auth-include'],
'options' => $this->groupSelect()
),
array(
'type' => 'switch',
'name' => 'INVITES-allow-delete-include',
'label' => 'Allow users to delete invites',
'help' => 'This must be disabled to enforce invitation limits.',
'value' => $this->config['INVITES-allow-delete-include']
),
array(
'type' => 'number',
'name' => 'INVITES-maximum-invites',
'label' => 'Maximum number of invites permitted for users.',
'help' => 'Set to 0 to disable the limit.',
'value' => $this->config['INVITES-maximum-invites'],
'placeholder' => '0'
),
),
'Plex Settings' => array(
array(
'type' => 'password-alt',
'name' => 'plexToken',
'label' => 'Plex Token',
'value' => $this->config['plexToken'],
'placeholder' => 'Use Get Token Button'
),
array(
'type' => 'button',
'label' => 'Get Plex Token',
'icon' => 'fa fa-ticket',
'text' => 'Retrieve',
'attr' => 'onclick="PlexOAuth(oAuthSuccess,oAuthError, oAuthMaxRetry, null, null, \'#INVITES-settings-items [name=plexToken]\')"'
),
array(
'type' => 'password-alt',
'name' => 'plexID',
'label' => 'Plex Machine',
'value' => $this->config['plexID'],
'placeholder' => 'Use Get Plex Machine Button'
),
array(
'type' => 'button',
'label' => 'Get Plex Machine',
'icon' => 'fa fa-id-badge',
'text' => 'Retrieve',
'attr' => 'onclick="showPlexMachineForm(\'#INVITES-settings-items [name=plexID]\')"'
),
array(
'type' => 'select2',
'class' => 'select2-multiple',
'id' => 'invite-select-' . $this->random_ascii_string(6),
'name' => 'INVITES-plexLibraries',
'label' => 'Libraries',
'value' => $this->config['INVITES-plexLibraries'],
'options' => $libraryList
),
array(
'type' => 'text',
'name' => 'INVITES-plex-tv-labels',
'label' => 'TV Labels (comma separated)',
'value' => $this->config['INVITES-plex-tv-labels'],
'placeholder' => 'All'
),
array(
'type' => 'text',
'name' => 'INVITES-plex-movies-labels',
'label' => 'Movies Labels (comma separated)',
'value' => $this->config['INVITES-plex-movies-labels'],
'placeholder' => 'All'
),
array(
'type' => 'text',
'name' => 'INVITES-plex-music-labels',
'label' => 'Music Labels (comma separated)',
'value' => $this->config['INVITES-plex-music-labels'],
'placeholder' => 'All'
),
array(
'type' => 'switch',
'name' => 'INVITES-add-plex-home',
'label' => 'When user subscribe add him to Plex Home',
'value' => $this->config['INVITES-add-plex-home']
)
),
'Komga Settings' => array(
array(
'type' => 'switch',
'name' => 'INVITES-komga-enabled',
'label' => 'Enable Komga for auto create account',
'value' => $this->config['INVITES-komga-enabled'],
),
array(
'type' => 'input',
'name' => 'INVITES-komga-uri',
'label' => 'URL',
'value' => $this->config['INVITES-komga-uri'],
'placeholder' => 'http(s)://hostname:port'
),
array(
'type' => 'password-alt',
'name' => 'INVITES-komga-api-key',
'label' => 'Komga Api Key',
'value' => $this->config['INVITES-komga-api-key']
),
array(
'type' => 'password-alt',
'name' => 'INVITES-komga-default-user-password',
'label' => 'Default password for new user',
'value' => $this->config['INVITES-komga-default-user-password']
),
array(
'type' => 'select2',
'class' => 'select2-multiple',
'id' => 'INVITES-select-' . $this->random_ascii_string(6),
'name' => 'INVITES-komga-roles',
'label' => 'Roles',
'value' => $this->config['INVITES-komga-roles'],
'options' => $komgaRoles
),
array(
'type' => 'select2',
'class' => 'select2-multiple',
'id' => 'INVITES-select-' . $this->random_ascii_string(6),
'name' => 'INVITES-komga-libraryIds',
'label' => 'Libraries',
'value' => $this->config['INVITES-komga-libraryIds'],
'options' => $komgalibrary
)
),
'Nextcloud Settings' => array(
array(
'type' => 'switch',
'name' => 'INVITES-nextcloud-enabled',
'label' => 'Enable Nextcloud for auto create account',
'value' => $this->config['INVITES-nextcloud-enabled'],
),
array(
'type' => 'switch',
'name' => 'INVITES-nextcloud-plex-sso',
'label' => 'Enable if you have Plex SSO app installed on Nextcloud',
'value' => $this->config['INVITES-nextcloud-plex-sso'],
),
array(
'type' => 'input',
'name' => 'INVITES-nextcloud-url',
'label' => 'Nextcloud URI',
'value' => $this->config['INVITES-nextcloud-url']
),
array(
'type' => 'input',
'name' => 'INVITES-nextcloud-admin-user',
'label' => 'Nextcloud Admin User',
'value' => $this->config['INVITES-nextcloud-admin-user']
),
array(
'type' => 'password',
'name' => 'INVITES-nextcloud-admin-password',
'label' => 'Nextcloud Admin Password',
'value' => $this->config['INVITES-nextcloud-admin-password']
),
array(
'type' => 'input',
'name' => 'INVITES-nextcloud-quota',
'label' => 'Storage quota for user after subscription (empty for no-limit)',
'value' => $this->config['INVITES-nextcloud-quota'],
'placeholder' => '10GB'
),
array(
'type' => 'select2',
'class' => 'select2-multiple',
'id' => 'INVITES-select-' . $this->random_ascii_string(6),
'name' => 'INVITES-nextcloud-groups-member',
'label' => 'Nextcloud Groups after subscription',
'value' => $this->config['INVITES-nextcloud-groups-member'],
'options' => $nextcloudRoles
)
),
'Emby Settings' => array(
array(
'type' => 'password-alt',
'name' => 'embyToken',
'label' => 'Emby API key',
'value' => $this->config['embyToken'],
'placeholder' => 'enter key from emby'
),
array(
'type' => 'text',
'name' => 'embyURL',
'label' => 'Emby server adress',
'value' => $this->config['embyURL'],
'placeholder' => 'localhost:8086'
),
array(
'type' => 'text',
'name' => 'INVITES-EmbyTemplate',
'label' => 'Emby User to be used as template for new users',
'value' => $this->config['INVITES-EmbyTemplate'],
'placeholder' => 'AdamSmith'
)
),
'FYI' => array(
array(
'type' => 'html',
'label' => 'Note',
'html' => 'After enabling for the first time, please reload the page - Menu is located under User menu on top right'
)
)
);
}
public function _invitesPluginAction($username, $action = null, $type = null, $mail)
{
if ($action == null) {
$this->setAPIResponse('error', 'No Action supplied', 409);
return false;
}
switch ($type) {
case 'plex':
if (!empty($this->config['plexToken']) && !empty($this->config['plexID'])) {
$url = "https://plex.tv/api/servers/" . $this->config['plexID'] . "/shared_servers/";
if ($this->config['INVITES-plexLibraries'] !== "") {
$libraries = explode(',', $this->config['INVITES-plexLibraries']);
} else {
$libraries = '';
}
if ($this->config['INVITES-plex-tv-labels'] !== "") {
$tv_labels = "label=" . $this->config['INVITES-plex-tv-labels'];
} else {
$tv_labels = "";
}
if ($this->config['INVITES-plex-movies-labels'] !== "") {
$movies_labels = "label=" . $this->config['INVITES-plex-movies-labels'];
} else {
$movies_labels = "";
}
if ($this->config['INVITES-plex-music-labels'] !== "") {
$music_labels = "label=" . $this->config['INVITES-plex-music-labels'];
} else {
$music_labels = "";
}
$headers = array(
"Accept" => "application/json",
"Content-Type" => "application/json",
"X-Plex-Token" => $this->config['plexToken']
);
$data = array(
"server_id" => $this->config['plexID'],
"shared_server" => array(
"library_section_ids" => $libraries,
"invited_email" => $username
),
"sharing_settings" => array(
"filterTelevision" => $tv_labels,
"filterMovies" => $movies_labels,
"filterMusic" => $music_labels
)
);
try {
switch ($action) {
case 'share':
$response = Requests::post($url, $headers, json_encode($data), array());
if($this->config['INVITES-add-plex-home']) {
$this->_addUserPlexHome($mail);
}
if($this->config['INVITES-komga-enabled']) {
$this->_createKomgaAccount($mail);
}
if ($this->config['INVITES-nextcloud-enabled']) {
$nextcloudAccountCreated = $this->_createNextcloudAccount($mail, $username);
}
break;
case 'unshare':
$id = (is_numeric($username) ? $username : $this->_invitesPluginConvertPlexName($username, "id"));
$url = $url . $id;
$response = Requests::delete($url, $headers, array());
break;
default:
$this->setAPIResponse('error', 'No Action supplied', 409);
return false;
}
if ($response->success) {
$this->setLoggerChannel('Invites')->info('Plex User now has access to system');
$this->setAPIResponse('success', 'Plex User now has access to system', 200);
return true;
} else {
switch ($response->status_code) {
case 400:
$this->setLoggerChannel('Plex')->warning('Plex User already has access');
$this->setAPIResponse('success', 'Plex User already has access', 200);
return true;
case 401:
$this->setLoggerChannel('Plex')->warning('Incorrect Token');
$this->setAPIResponse('error', 'Incorrect Token', 409);
return false;
case 404:
$this->setLoggerChannel('Plex')->warning('Libraries not setup correctly');
$this->setAPIResponse('error', 'Libraries not setup correct', 409);
return false;
default:
$this->setLoggerChannel('Plex')->warning('An error occurred [' . $response->status_code . ']');
$this->setAPIResponse('error', 'An Error Occurred', 409);
return false;
}
}
} catch (Requests_Exception $e) {
$this->setLoggerChannel('Plex')->error($e);
$this->setAPIResponse('error', $e->getMessage(), 409);
return false;
}
} else {
$this->setLoggerChannel('Plex')->warning('Plex Token/ID not set');
$this->setAPIResponse('error', 'Plex Token/ID not set', 409);
return false;
}
break;
case 'emby':
try {
#add emby user to system
$this->setAPIResponse('success', 'User now has access to system', 200);
return true;
} catch (Requests_Exception $e) {
$this->setLoggerChannel('Emby')->error($e);
$this->setAPIResponse('error', $e->getMessage(), 409);
return false;
}
default:
return false;
}
return false;
}
public function _invitesPluginConvertPlexName($user, $type)
{
$array = $this->userList('plex');
switch ($type) {
case "username":
case "u":
$plexUser = array_search($user, $array['users']);
break;
case "id":
if (array_key_exists(strtolower($user), $array['users'])) {
$plexUser = $array['users'][strtolower($user)];
}
break;
default:
$plexUser = false;
}
return (!empty($plexUser) ? $plexUser : null);
}
/**
* Creates a new Komga user account using the provided email address.
*
* @param string $email The email address for the new Komga user account.
* @return bool True if the account was successfully created, false otherwise.
*/
private function _createKomgaAccount($email) {
$this->logger->info('Try to create Komga account for ' . $email);
if(!$this->_checkKomgaVar()) {
return false;
}
if (empty($email)) {
$this->setLoggerChannel('Invites')->info('User email empty');
return false;
}
$endpoint = rtrim($this->config['INVITES-komga-uri'], '/') . '/api/v2/users';
$apiKey = $this->config['INVITES-komga-api-key'];
$password = $this->decrypt($this->config['INVITES-komga-default-user-password']);
$rolesStr = $this->config['INVITES-komga-roles'] ?? '';
$roles = array_values(array_filter(array_map('trim', explode(';', $rolesStr))));
$libIdsStr = $this->config['INVITES-komga-libraryIds'] ?? '';
$libraryIds = array_values(array_filter(array_map('trim', explode(';', $libIdsStr))));
$headers = array(
'accept' => 'application/json',
'X-API-Key' => $apiKey,
'Content-Type' => 'application/json'
);
$payload = array(
'email' => $email,
'password' => $password,
'roles' => $roles,
'sharedLibraries' => array(
'all' => false,
'libraryIds' => $libraryIds
)
);
try {
$response = Requests::post($endpoint, $headers, json_encode($payload));
if ($response->success) {
$this->setLoggerChannel('Komga')->info('User created ' . $email . ' with roles: ' . implode(',', $roles) . ' and libraries: ' . implode(',', $libraryIds));
return true;
}
$this->setLoggerChannel('Komga')->warning('User not created ' . $email . ' HTTP ' . $response->status_code);
} catch (Requests_Exception $e) {
$this->setLoggerChannel('Komga')->error('User not created ' . $email . ' Requests_Exception: ' . $e->getMessage());
}
return false;
}
/**
* Retrieves a list of Komga roles
*
* @return array An array of associative arrays, each containing 'name' and 'value' for a Komga role.
*/
public function _getKomgaRoles() {
$komgaRoles = array();
$roleNames = array(
'ADMIN' => 'Administrator',
'FILE_DOWNLOAD' => 'File download',
'PAGE_STREAMING' => 'Page streaming',
'KOBO_SYNC' => 'Kobo Sync',
'KOREADER_SYNC' => 'Koreader Sync'
);
foreach ($roleNames as $value => $name) {
$komgaRoles[] = array(
'name' => $name,
'value' => $value
);
}
return $komgaRoles;
}
/**
* Fetches the list of Komga libraries from the Komga API.
*
* @return array|false Returns an array of libraries with 'name' and 'id' on success, or false on failure.
*/
public function _getKomgaLibraries() {
$this->logger->info('Try to fetch Komga libraries');
if(!$this->_checkKomgaVar()) {
return false;
}
$endpoint = rtrim($this->config['komgaURL'], '/') . '/api/v1/libraries';
$apiKey = $this->config['INVITES-komga-api-key'];
$libraryListDefault = array(
array(
'name' => 'Refresh page to update List',
'value' => '',
'disabled' => true,
),
);
$headers = array(
'accept' => 'application/json',
'X-API-Key' => $apiKey
);
try {
$response = Requests::get($endpoint, $headers);
if ($response->success) {
$libraries = json_decode($response->body, true);
// Komga retourne un tableau d'objets librairie
$result = array();
foreach ($libraries as $library) {
$result[] = array(
'name' => $library['name'],
'id' => $library['id']
);
}
$this->logger->info('Fetched libraries: ' . json_encode($result));
if(!empty($result)) {
return $result;
}
} else {
$this->logger->warning("Error HTTP ".$response->status_code.' body='.$response->body);
}
} catch (Requests_Exception $e) {
$this->logger->warning("Exception: " . $e->getMessage());
}
return $libraryListDefault;
}
/**
* Fetches the list of Nextcloud groups using the configured Nextcloud admin credentials.
*
* @return array|false Returns an array of groups with 'name' and 'value' keys on success,
* or false on failure.
*/
public function _getNextcloudGroups() {
$this->logger->info('Try to fetch Nextcloud groups');
if(!$this->_checkNextcloudVar()) {
return false;
}
$url = rtrim($this->config['INVITES-nextcloud-url'], '/') . '/ocs/v1.php/cloud/groups';
$adminUser = $this->config['INVITES-nextcloud-admin-user'];
$adminPass = $this->decrypt($this->config['INVITES-nextcloud-admin-password']);
$headers = array(
'OCS-APIRequest' => 'true',
'Accept' => 'application/json',
);
try {
$options = array(
'auth' => array($adminUser, $adminPass),
);
$response = Requests::get($url, $headers, $options);
if ($response->success) {
$body = json_decode($response->body, true);
if (isset($body['ocs']['data']['groups'])) {
$this->logger->info('Fetched groups: ' . implode(', ', $body['ocs']['data']['groups']));
$groups = $body['ocs']['data']['groups'];
$result = array();
foreach ($groups as $group) {
$result[] = array(
'name' => $group,
'value' => $group
);
}
return $result;
} else {
$this->logger->warning('Groups not found in response');
}
} else {
$this->logger->warning("Error HTTP ".$response->status_code.' body='.$response->body);
}
} catch (Requests_Exception $e) {
$this->logger->warning("Exception: " . $e->getMessage());
}
return false;
}
/**
* Checks if all required Nextcloud configuration variables are set.
*
* @return bool Returns true if all required Nextcloud configuration variables are set; false otherwise.
*/
public function _checkNextcloudVar() {
if (empty($this->config['INVITES-nextcloud-enabled'])) {
$this->logger->info('Nextcloud disabled in config');
return false;
}
if (empty($this->config['INVITES-nextcloud-url'])) {
$this->logger->info('Nextcloud URL missing');
return false;
}
if (empty($this->config['INVITES-nextcloud-admin-user']) || empty($this->config['INVITES-nextcloud-admin-password'])) {
$this->logger->info('Nextcloud admin credentials missing');
return false;
}
return true;
}
/**
* Checks if all required komga configuration variables are set.
*
* @return bool Returns true if all required komga configuration variables are set; false otherwise.
*/
public function _checkKomgaVar() {
if (empty($this->config['INVITES-komga-uri'])) {
$this->setLoggerChannel('Invites')->info('Komga uri is missing');
return false;
}
if (empty($this->config['INVITES-komga-api-key'])) {
$this->setLoggerChannel('Invites')->info('Komga api key is missing');
return false;
}
if (empty($this->config['INVITES-komga-roles'])) {
$this->setLoggerChannel('Invites')->info('Komga roles empty');
return false;
}
if (empty($this->config['INVITES-komga-libraryIds'])) {
$this->setLoggerChannel('Invites')->info('Komga library empty');
return false;
}
if (empty($this->config['INVITES-komga-default-user-password'])) {
$this->setLoggerChannel('Invites')->info('Komga default user password empty');
return false;
}
return true;
}
/**
* Creates a Nextcloud account for a user based on their email and other parameters.
*
* @param string $email The email address of the user to create in Nextcloud.
* @param string $displayName The display name for the Nextcloud user.
* @param string $nextcloudGroupsMember A semicolon-separated list of Nextcloud groups to add the user to.
* @param string $nextcloudQuota The storage quota to assign to the user (e.g., "5GB").
*
* @return bool Returns true if the account was successfully created, false otherwise.
*/
public function _createNextcloudAccount($email, $displayName) {
$this->logger->info('Try to create Nextcloud account');
if(!$this->_checkNextcloudVar()) {
return false;
}
$nextcloudGroupsMember = $this->config['INVITES-nextcloud-groups-member'] ?? '';
$nextcloudQuota = $this->config['INVITES-nextcloud-quota'] ?? '';
$userid = $email;
if($this->config['INVITES-nextcloud-plex-sso']) {
$plexUserId = $this->_getPlexUserIdByEmail($email);
$this->logger->warning('plexUserId=' . $plexUserId);
if (!empty($plexUserId)) {
$userid = 'PlexTv-' . $plexUserId;
}
}
try {
$password = bin2hex(random_bytes(12));
} catch (\Throwable $e) {
$password = bin2hex(openssl_random_pseudo_bytes(12));
}
if (empty($password)) {
$this->logger->warning('Error generating password');
return false;
}
$url = rtrim($this->config['INVITES-nextcloud-url'], '/') . '/ocs/v1.php/cloud/users';
$adminUser = $this->config['INVITES-nextcloud-admin-user'];
$adminPass = $this->decrypt($this->config['INVITES-nextcloud-admin-password']);
$headers = array(
'OCS-APIRequest' => 'true',
'Accept' => 'application/json',
);
$data = array(
'userid' => $userid,
'password' => $password,
'email' => $email,
'displayName' => $displayName,
);
if (!empty($nextcloudGroupsMember)) {
$groups = array_values(array_filter(array_map('trim', explode(';', $nextcloudGroupsMember))));
foreach ($groups as $group) {
$data['groups[]'] = $group;
}
}
if (!empty($nextcloudQuota)) {
$data['quota'] = $nextcloudQuota;
}
try {
$options = array(
'auth' => array($adminUser, $adminPass),
);
$response = Requests::post($url, $headers, $data, $options);
if ($response->success) {
$this->logger->info("User created ($email)");
return true;
}
$this->logger->warning("Error ($email) HTTP ".$response->status_code.' body='.$response->body);
} catch (Requests_Exception $e) {
$this->logger->warning("Exception: " . $e->getMessage());
}
return false;
}
/**
* Retrieves the Plex user ID associated with a given email address.
*
* @param string $email The email address to search for in the shared Plex users.
* @return string|null The Plex user ID if found, or null if not found or on error.
*/
public function _getPlexUserIdByEmail($email) {
$this->logger->info("Try to get Plex userID for $email");
if (empty($this->config['plexToken']) || empty($this->config['plexID'])) {
$this->logger->warning("PlexToken ou plexID missing");
return null;
}
$url = "https://clients.plex.tv/api/invites/requested";
$headers = array(
"Accept" => "application/json",
"X-Plex-Token" => $this->config['plexToken']
);
try {
$response = Requests::get($url, $headers);
if ($response->success) {
$xml = simplexml_load_string($response->body);
// Parcourt les éléments du MediaContainer
foreach ($xml->Invite as $invite) {
$inviteEmail = (string)$invite['email'];
$inviteId = (string)$invite['id'];
if (strcasecmp($inviteEmail, $email) === 0) {
$this->logger->info("Find id=$inviteId for $email");
return $inviteId;
}
}
}
$this->logger->warning("No userId found for $email");
} catch (Requests_Exception $e) {
$this->logger->warning("Exception: " . $e->getMessage());
}
return null;
}
/**
* Retrieves the email address associated with a given invite code.
*
* @param string $inviteCode The invite code to look up.
* @return string|false The email address associated with the invite code, or false if not found.
*/
public function _getEmailFronInviteCode($inviteCode) {
if (empty($inviteCode)) {
$this->logger->warning('Invite code not found');
return false;
}
$emailLookupQuery = [
array(
'function' => 'fetch',
'query' => array(
'SELECT email FROM invites WHERE code = ? COLLATE NOCASE',
$inviteCode
)
)
];
$emailRow = $this->processQueries($emailLookupQuery);
if ($emailRow && !empty($emailRow['email'])) {
$this->logger->info("Email foud via the code [$inviteCode] : ".$emailRow['email']);
return $emailRow['email'];
} else {
$this->logger->warning("No mail found for the code [$inviteCode]");
return false;
}
}
/**
* Adds a user to Plex Home using the provided email address.
*
* @param string $email The email address of the user to invite.
* @return array|false Returns the decoded response from Plex API on success, or false on failure.
*/
public function _addUserPlexHome($email){
if (empty($email) || empty($this->config['plexToken'])) {
$this->logger->warning('_addUserPlexHome: email or plexToken missing');
return false;
}
$url = 'https://clients.plex.tv/api/home/users?invitedEmail=' . urlencode($email) . '&skipFriendship=1&X-Plex-Token=' . urlencode($this->config['plexToken']);
try {
$response = Requests::post($url, $headers);
if ($response->success) {
$this->logger->info('User added on plex home');
return json_decode($response->body, true);
} else {
$this->logger->info('_getPlexHomeUserByEmail: error (HTTP ' . $response->status_code . ')');
}
} catch (Requests_Exception $e) {
$this->logger->info('_addUserPlexHome: ' . $e->getMessage());
}
return false;
}
}