Files
Piwigo/include/functions_user.inc.php
Linty e0a2a0ba2b fixes #2449 enhance password reset flow with verification and lockout
Added email notification for successful password reset, improved verification code handling, and implemented account lockout after too many failed attempts. Introduced new language strings for user feedback and security messages. Refactored password reset logic to better handle guest/generic users and API key recommendations.
2025-11-17 21:43:14 +01:00

2919 lines
73 KiB
PHP

<?php
// +-----------------------------------------------------------------------+
// | This file is part of Piwigo. |
// | |
// | For copyright and license information, please view the COPYING.txt |
// | file that was distributed with this source code. |
// +-----------------------------------------------------------------------+
/**
* @package functions\user
*/
/**
* Checks if an email is well formed and not already in use.
*
* @param int $user_id
* @param string $mail_address
* @return string|void error message or nothing
*/
function validate_mail_address($user_id, $mail_address)
{
global $conf;
if (empty($mail_address) and
!($conf['obligatory_user_mail_address'] and
in_array(script_basename(), array('register', 'profile'))))
{
return '';
}
if ( !email_check_format($mail_address) )
{
return l10n('mail address must be like xxx@yyy.eee (example : jack@altern.org)');
}
if (defined("PHPWG_INSTALLED") and !empty($mail_address))
{
$query = '
SELECT count(*)
FROM '.USERS_TABLE.'
WHERE upper('.$conf['user_fields']['email'].') = upper(\''.$mail_address.'\')
'.(is_numeric($user_id) ? 'AND '.$conf['user_fields']['id'].' != \''.$user_id.'\'' : '').'
;';
list($count) = pwg_db_fetch_row(pwg_query($query));
if ($count != 0)
{
return l10n('this email address is already in use');
}
}
}
/**
* Checks if a login is not already in use.
* Comparision is case insensitive.
*
* @param string $login
* @return string|void error message or nothing
*/
function validate_login_case($login)
{
global $conf;
if (defined("PHPWG_INSTALLED"))
{
$query = "
SELECT ".$conf['user_fields']['username']."
FROM ".USERS_TABLE."
WHERE LOWER(".stripslashes($conf['user_fields']['username']).") = '".strtolower($login)."'
;";
$count = pwg_db_num_rows(pwg_query($query));
if ($count > 0)
{
return l10n('this login is already used');
}
}
}
/**
* Searches for user with the same username in different case.
*
* @param string $username typically typed in by user for identification
* @return string $username found in database
*/
function search_case_username($username)
{
global $conf;
$username_lo = strtolower($username);
$SCU_users = array();
$q = pwg_query("
SELECT ".$conf['user_fields']['username']." AS username
FROM `".USERS_TABLE."`;
");
while ($r = pwg_db_fetch_assoc($q))
$SCU_users[$r['username']] = strtolower($r['username']);
// $SCU_users is now an associative table where the key is the account as
// registered in the DB, and the value is this same account, in lower case
$users_found = array_keys($SCU_users, $username_lo);
// $users_found is now a table of which the values are all the accounts
// which can be written in lowercase the same way as $username
if (count($users_found) != 1) // If ambiguous, don't allow lowercase writing
return $username; // but normal writing will work
else
return $users_found[0];
}
/**
* Creates a new user.
*
* @param string $login
* @param string $password
* @param string $mail_adress
* @param bool $notify_admin
* @param array &$errors populated with error messages
* @param bool $notify_user
* @return int|false user id or false
*/
function register_user($login, $password, $mail_address, $notify_admin=true, &$errors = array(), $notify_user=false)
{
global $conf;
if ($login == '')
{
$errors[] = l10n('Please, enter a login');
}
if (preg_match('/^.* $/', $login))
{
$errors[] = l10n('login mustn\'t end with a space character');
}
if (preg_match('/^ .*$/', $login))
{
$errors[] = l10n('login mustn\'t start with a space character');
}
if (get_userid($login))
{
$errors[] = l10n('this login is already used');
}
if ($login != strip_tags($login))
{
$errors[] = l10n('html tags are not allowed in login');
}
$mail_error = validate_mail_address(null, $mail_address);
if ('' != $mail_error)
{
$errors[] = $mail_error;
}
if ($conf['insensitive_case_logon'] == true)
{
$login_error = validate_login_case($login);
if ($login_error != '')
{
$errors[] = $login_error;
}
}
$errors = trigger_change(
'register_user_check',
$errors,
array(
'username'=>$login,
'password'=>$password,
'email'=>$mail_address,
)
);
// if no error until here, registration of the user
if (empty($errors))
{
$insert = array(
$conf['user_fields']['username'] => $login,
$conf['user_fields']['password'] => $conf['password_hash']($password),
$conf['user_fields']['email'] => $mail_address
);
single_insert(USERS_TABLE, $insert);
$user_id = pwg_db_insert_id();
// Assign by default groups
$query = '
SELECT id
FROM `'.GROUPS_TABLE.'`
WHERE is_default = \''.boolean_to_string(true).'\'
ORDER BY id ASC
;';
$result = pwg_query($query);
$inserts = array();
while ($row = pwg_db_fetch_assoc($result))
{
$inserts[] = array(
'user_id' => $user_id,
'group_id' => $row['id']
);
}
if (count($inserts) != 0)
{
mass_inserts(USER_GROUP_TABLE, array('user_id', 'group_id'), $inserts);
}
$override = array();
if ($conf['browser_language'] and $language = get_browser_language())
{
$override['language'] = $language;
}
create_user_infos($user_id, $override);
if ($notify_admin and 'none' != $conf['email_admin_on_new_user'])
{
include_once(PHPWG_ROOT_PATH.'include/functions_mail.inc.php');
$admin_url = get_absolute_root_url().'admin.php?page=user_list&user_id='.$user_id;
$keyargs_content = array(
get_l10n_args('User: %s', stripslashes($login) ),
get_l10n_args('Email: %s', $mail_address),
get_l10n_args(''),
get_l10n_args('Admin: %s', $admin_url),
);
$group_id = null;
if (preg_match('/^group:(\d+)$/', $conf['email_admin_on_new_user'], $matches))
{
$group_id = $matches[1];
}
pwg_mail_notification_admins(
get_l10n_args('Registration of %s', stripslashes($login) ),
$keyargs_content,
true, // $send_technical_details
$group_id
);
}
if ($notify_user and email_check_format($mail_address))
{
include_once(PHPWG_ROOT_PATH.'include/functions_mail.inc.php');
$length = rand(10, 15);
$keyargs_content = array(
get_l10n_args('Hello %s,', stripslashes($login)),
get_l10n_args('Thank you for registering at %s!', $conf['gallery_title']),
get_l10n_args('', ''),
get_l10n_args('Here are your connection settings', ''),
get_l10n_args('', ''),
get_l10n_args('Link: %s', get_absolute_root_url()),
get_l10n_args('Username: %s', stripslashes($login)),
get_l10n_args('Password: %s', str_repeat("*", $length)),
get_l10n_args('Email: %s', $mail_address),
get_l10n_args('', ''),
get_l10n_args('If you think you\'ve received this email in error, please contact us at %s', get_webmaster_mail_address()),
);
pwg_mail(
$mail_address,
array(
'subject' => '['.$conf['gallery_title'].'] '.l10n('Registration'),
'content' => l10n_args($keyargs_content),
'content_format' => 'text/plain',
)
);
}
trigger_notify(
'register_user',
array(
'id'=>$user_id,
'username'=>$login,
'email'=>$mail_address,
)
);
pwg_activity('user', $user_id, 'add');
return $user_id;
}
else
{
return false;
}
}
/**
* Fetches user data from database.
* Same that getuserdata() but with additional tests for guest.
*
* @param int $user_id
* @param boolean $user_cache
* @return array
*/
function build_user($user_id, $use_cache=true)
{
global $conf;
$user['id'] = $user_id;
$user = array_merge( $user, getuserdata($user_id, $use_cache) );
if ($user['id'] == $conf['guest_id'] and $user['status'] <> 'guest')
{
$user['status'] = 'guest';
$user['internal_status']['guest_must_be_guest'] = true;
}
// Check user theme. 2 possible problems:
// 1. the user_infos.theme was not found in the themes table, thus themes.name is null
// 2. the theme is not really installed on the filesystem
if (!isset($user['theme_name']) or !check_theme_installed($user['theme']))
{
$user['theme'] = get_default_theme();
$user['theme_name'] = $user['theme'];
}
return $user;
}
/**
* Finds informations related to the user identifier.
*
* @param int $user_id
* @param boolean $use_cache
* @return array
*/
function getuserdata($user_id, $use_cache=false)
{
global $conf, $logger;
// retrieve basic user data
$query = '
SELECT ';
$is_first = true;
foreach ($conf['user_fields'] as $pwgfield => $dbfield)
{
if ($is_first)
{
$is_first = false;
}
else
{
$query.= '
, ';
}
$query.= $dbfield.' AS '.$pwgfield;
}
$query.= '
FROM '.USERS_TABLE.'
WHERE '.$conf['user_fields']['id'].' = \''.$user_id.'\'';
$row = pwg_db_fetch_assoc(pwg_query($query));
// retrieve additional user data ?
if ($conf['external_authentification'])
{
$query = '
SELECT
COUNT(1) AS counter
FROM '.USER_INFOS_TABLE.' AS ui
LEFT JOIN '.USER_CACHE_TABLE.' AS uc ON ui.user_id = uc.user_id
LEFT JOIN '.THEMES_TABLE.' AS t ON t.id = ui.theme
WHERE ui.user_id = '.$user_id.'
GROUP BY ui.user_id
;';
list($counter) = pwg_db_fetch_row(pwg_query($query));
if ($counter != 1)
{
create_user_infos($user_id);
}
}
// retrieve user info
$query = '
SELECT
ui.*,
uc.*,
t.name AS theme_name
FROM '.USER_INFOS_TABLE.' AS ui
LEFT JOIN '.USER_CACHE_TABLE.' AS uc ON ui.user_id = uc.user_id
LEFT JOIN '.THEMES_TABLE.' AS t ON t.id = ui.theme
WHERE ui.user_id = '.$user_id.'
;';
$result = pwg_query($query);
$user_infos_row = pwg_db_fetch_assoc($result);
// then merge basic + additional user data
$userdata = array_merge($row, $user_infos_row);
foreach ($userdata as &$value)
{
// If the field is true or false, the variable is transformed into a boolean value.
if ($value == 'true')
{
$value = true;
}
elseif ($value == 'false')
{
$value = false;
}
}
unset($value);
$userdata['preferences'] = empty($userdata['preferences']) ? array() : unserialize($userdata['preferences']);
if ($use_cache)
{
$generate_user_cache = false;
$cache_generation_token_name = 'generate_user_cache-u'.$userdata['id'];
$exec_code = substr(sha1(random_bytes(1000)), 0, 4);
$logger_msg_prefix = '['.__FUNCTION__.'][exec_code='.$exec_code.'][user_id='.$userdata['id'].'] ';
if (!isset($userdata['need_update'])
or !is_bool($userdata['need_update'])
or $userdata['need_update'] == true)
{
$logger->info($logger_msg_prefix.'needs user_cache to be rebuilt');
$exec_id = pwg_unique_exec_begins($cache_generation_token_name);
if (false === $exec_id)
{
$logger->info($logger_msg_prefix.'starts to wait for another request to build user_cache');
$user_cache_waiting_start_time = get_moment();
for ($k = 0; $k < 20; $k++)
{
sleep(1);
$query = '
SELECT
COUNT(*)
FROM '.USER_CACHE_TABLE.'
WHERE user_id='.$userdata['id'].'
;';
list($nb_cache_lines) = pwg_db_fetch_row(pwg_query($query));
$logger_msg = $logger_msg_prefix.'user_cache generation waiting k='.$k.' ';
$waiting_time = get_elapsed_time($user_cache_waiting_start_time, get_moment());
if ($nb_cache_lines > 0)
{
$logger->info($logger_msg.'user_cache rebuilt, after waiting '.$waiting_time);
return getuserdata($user_id, false);
}
elseif (!pwg_unique_exec_is_running($cache_generation_token_name))
{
$logger->info($logger_msg.'user_cache rebuilt but has been reset since, give it another try, after waiting '.$waiting_time);
return getuserdata($user_id, true);
}
else
{
$logger->info($logger_msg.'user_cache not ready yet, after waiting '.$waiting_time);
}
}
$logger->info($logger_msg_prefix.'user_cache generation waiting has timed out after '.get_elapsed_time($user_cache_waiting_start_time, get_moment()));
set_status_header(503, 'Service Unavailable');
@header('Retry-After: 900');
header('Content-Type: text/html; charset='.get_pwg_charset());
echo l10n('Rebuilding user cache takes long. Please, come back later.');
echo str_repeat( ' ', 512); //IE6 doesn't error output if below a size
exit();
}
else
{
$generate_user_cache = true;
}
}
if ($generate_user_cache)
{
$user_cache_generation_start_time = get_moment();
$userdata['cache_update_time'] = time();
// Set need update are done
$userdata['need_update'] = false;
$userdata['forbidden_categories'] =
calculate_permissions($userdata['id'], $userdata['status']);
/* now we build the list of forbidden images (this list does not contain
images that are not in at least an authorized category)*/
$query = '
SELECT DISTINCT(id)
FROM '.IMAGES_TABLE.' INNER JOIN '.IMAGE_CATEGORY_TABLE.' ON id=image_id
WHERE category_id NOT IN ('.$userdata['forbidden_categories'].')
AND level>'.$userdata['level'];
$forbidden_ids = query2array($query,null, 'id');
if ( empty($forbidden_ids) )
{
$forbidden_ids[] = 0;
}
$userdata['image_access_type'] = 'NOT IN'; //TODO maybe later
$userdata['image_access_list'] = implode(',',$forbidden_ids);
$query = '
SELECT COUNT(DISTINCT(image_id)) as total
FROM '.IMAGE_CATEGORY_TABLE.'
WHERE category_id NOT IN ('.$userdata['forbidden_categories'].')
AND image_id '.$userdata['image_access_type'].' ('.$userdata['image_access_list'].')';
list($userdata['nb_total_images']) = pwg_db_fetch_row(pwg_query($query));
// now we update user cache categories
$user_cache_cats = get_computed_categories($userdata, null);
if ( !is_admin($userdata['status']) )
{ // for non admins we forbid categories with no image (feature 1053)
$forbidden_ids = array();
foreach ($user_cache_cats as $cat)
{
if ($cat['count_images']==0)
{
$forbidden_ids[] = $cat['cat_id'];
remove_computed_category($user_cache_cats, $cat);
}
}
if ( !empty($forbidden_ids) )
{
if ( empty($userdata['forbidden_categories']) )
{
$userdata['forbidden_categories'] = implode(',', $forbidden_ids);
}
else
{
$userdata['forbidden_categories'] .= ','.implode(',', $forbidden_ids);
}
}
}
// delete user cache
$query = '
DELETE FROM '.USER_CACHE_CATEGORIES_TABLE.'
WHERE user_id = '.$userdata['id'];
pwg_query($query);
// Due to concurrency issues, we ask MySQL to ignore errors on
// insert. This may happen when cache needs refresh and that Piwigo is
// called "very simultaneously".
mass_inserts(
USER_CACHE_CATEGORIES_TABLE,
array(
'user_id', 'cat_id',
'date_last', 'max_date_last', 'nb_images', 'count_images', 'nb_categories', 'count_categories'
),
$user_cache_cats,
array('ignore' => true)
);
// update user cache
$query = '
DELETE FROM '.USER_CACHE_TABLE.'
WHERE user_id = '.$userdata['id'];
pwg_query($query);
// for the same reason as user_cache_categories, we ignore error on
// this insert
$query = '
INSERT IGNORE INTO '.USER_CACHE_TABLE.'
(user_id, need_update, cache_update_time, forbidden_categories, nb_total_images,
last_photo_date,
image_access_type, image_access_list)
VALUES
('.$userdata['id'].',\''.boolean_to_string($userdata['need_update']).'\','
.$userdata['cache_update_time'].',\''
.$userdata['forbidden_categories'].'\','.$userdata['nb_total_images'].','.
(empty($userdata['last_photo_date']) ? 'NULL': '\''.$userdata['last_photo_date'].'\'').
',\''.$userdata['image_access_type'].'\',\''.$userdata['image_access_list'].'\')';
pwg_query($query);
pwg_unique_exec_ends($cache_generation_token_name);
$logger->info($logger_msg_prefix.'user_cache generated, executed in '.get_elapsed_time($user_cache_generation_start_time, get_moment()));
}
}
return $userdata;
}
/**
* Deletes favorites of the current user if he's not allowed to see them.
*/
function check_user_favorites()
{
global $user;
if ($user['forbidden_categories'] == '')
{
return;
}
// $filter['visible_categories'] and $filter['visible_images']
// must be not used because filter <> restriction
// retrieving images allowed : belonging to at least one authorized
// category
$query = '
SELECT DISTINCT f.image_id
FROM '.FAVORITES_TABLE.' AS f INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic
ON f.image_id = ic.image_id
WHERE f.user_id = '.$user['id'].'
'.get_sql_condition_FandF(
array(
'forbidden_categories' => 'ic.category_id',
),
'AND'
).'
;';
$authorizeds = query2array($query,null, 'image_id');
$query = '
SELECT image_id
FROM '.FAVORITES_TABLE.'
WHERE user_id = '.$user['id'].'
;';
$favorites = query2array($query,null, 'image_id');
$to_deletes = array_diff($favorites, $authorizeds);
if (count($to_deletes) > 0)
{
$query = '
DELETE FROM '.FAVORITES_TABLE.'
WHERE image_id IN ('.implode(',', $to_deletes).')
AND user_id = '.$user['id'].'
;';
pwg_query($query);
}
}
/**
* Calculates the list of forbidden categories for a given user.
*
* Calculation is based on private categories minus categories authorized to
* the groups the user belongs to minus the categories directly authorized
* to the user. The list contains at least 0 to be compliant with queries
* such as "WHERE category_id NOT IN ($forbidden_categories)"
*
* @param int $user_id
* @param string $user_status
* @return string comma separated ids
*/
function calculate_permissions($user_id, $user_status)
{
$query = '
SELECT id
FROM '.CATEGORIES_TABLE.'
WHERE status = \'private\'
;';
$private_array = query2array($query,null, 'id');
// retrieve category ids directly authorized to the user
$query = '
SELECT cat_id
FROM '.USER_ACCESS_TABLE.'
WHERE user_id = '.$user_id.'
;';
$authorized_array = query2array($query,null, 'cat_id');
// retrieve category ids authorized to the groups the user belongs to
$query = '
SELECT cat_id
FROM '.USER_GROUP_TABLE.' AS ug INNER JOIN '.GROUP_ACCESS_TABLE.' AS ga
ON ug.group_id = ga.group_id
WHERE ug.user_id = '.$user_id.'
;';
$authorized_array =
array_merge(
$authorized_array,
query2array($query,null, 'cat_id')
);
// uniquify ids : some private categories might be authorized for the
// groups and for the user
$authorized_array = array_unique($authorized_array);
// only unauthorized private categories are forbidden
$forbidden_array = array_diff($private_array, $authorized_array);
// if user is not an admin, locked categories are forbidden
if (!is_admin($user_status))
{
$query = '
SELECT id
FROM '.CATEGORIES_TABLE.'
WHERE visible = \'false\'
;';
$forbidden_array = array_merge($forbidden_array, query2array($query, null, 'id') );
$forbidden_array = array_unique($forbidden_array);
}
if ( empty($forbidden_array) )
{// at least, the list contains 0 value. This category does not exists so
// where clauses such as "WHERE category_id NOT IN(0)" will always be
// true.
$forbidden_array[] = 0;
}
return implode(',', $forbidden_array);
}
/**
* Returns user identifier thanks to his name.
*
* @param string $username
* @param int|false
*/
function get_userid($username)
{
global $conf;
$username = pwg_db_real_escape_string($username);
$query = '
SELECT '.$conf['user_fields']['id'].'
FROM '.USERS_TABLE.'
WHERE '.$conf['user_fields']['username'].' = \''.$username.'\'
;';
$result = pwg_query($query);
if (pwg_db_num_rows($result) == 0)
{
return false;
}
else
{
list($user_id) = pwg_db_fetch_row($result);
return $user_id;
}
}
/**
* Returns user identifier thanks to his email.
*
* @param string $email
* @param int|false
*/
function get_userid_by_email($email)
{
global $conf;
$email = pwg_db_real_escape_string($email);
$query = '
SELECT
'.$conf['user_fields']['id'].'
FROM '.USERS_TABLE.'
WHERE UPPER('.$conf['user_fields']['email'].') = UPPER(\''.$email.'\')
;';
$result = pwg_query($query);
if (pwg_db_num_rows($result) == 0)
{
return false;
}
else
{
list($user_id) = pwg_db_fetch_row($result);
return $user_id;
}
}
/**
* Returns a array with default user valuees.
*
* @param convert_str ceonferts 'true' and 'false' into booleans
* @return array
*/
function get_default_user_info($convert_str=true)
{
global $cache, $conf;
if (!isset($cache['default_user']))
{
$query = '
SELECT *
FROM '.USER_INFOS_TABLE.'
WHERE user_id = '.$conf['default_user_id'].'
;';
$result = pwg_query($query);
if (pwg_db_num_rows($result) > 0)
{
$cache['default_user'] = pwg_db_fetch_assoc($result);
unset($cache['default_user']['user_id']);
unset($cache['default_user']['status']);
unset($cache['default_user']['registration_date']);
unset($cache['default_user']['last_visit']);
unset($cache['default_user']['last_visit_from_history']);
}
else
{
$cache['default_user'] = false;
}
}
if (is_array($cache['default_user']) and $convert_str)
{
$default_user = $cache['default_user'];
foreach ($default_user as &$value)
{
// If the field is true or false, the variable is transformed into a boolean value.
if ($value == 'true')
{
$value = true;
}
elseif ($value == 'false')
{
$value = false;
}
}
return $default_user;
}
else
{
return $cache['default_user'];
}
}
/**
* Returns a default user value.
*
* @param string $value_name
* @param mixed $default
* @return mixed
*/
function get_default_user_value($value_name, $default)
{
$default_user = get_default_user_info(true);
if ($default_user === false or empty($default_user[$value_name]))
{
return $default;
}
else
{
return $default_user[$value_name];
}
}
/**
* Returns the default theme.
* If the default theme is not available it returns the first available one.
*
* @return string
*/
function get_default_theme()
{
$theme = get_default_user_value('theme', PHPWG_DEFAULT_TEMPLATE);
if (check_theme_installed($theme))
{
return $theme;
}
// let's find the first available theme
$active_themes = array_keys(get_pwg_themes());
return isset($active_themes[0]) ? $active_themes[0] : 'default';
}
/**
* Returns the default language.
*
* @return string
*/
function get_default_language()
{
return get_default_user_value('language', PHPWG_DEFAULT_LANGUAGE);
}
/**
* Tries to find the browser language among available languages.
*
* @return string
*/
function get_browser_language()
{
$language_header = @$_SERVER['HTTP_ACCEPT_LANGUAGE'];
if ($language_header == '')
{
return false;
}
// case insensitive match
// 'en-US;q=0.9, fr-CH, kok-IN;q=0.7' => 'en_us;q=0.9, fr_ch, kok_in;q=0.7'
$language_header = strtolower(str_replace("-", "_", $language_header));
$match_pattern = '/(([a-z]{1,8})(?:_[a-z0-9]{1,8})*)\s*(?:;\s*q\s*=\s*([01](?:\.[0-9]{0,3})?))?/';
$matches = null;
preg_match_all($match_pattern, $language_header, $matches);
$accept_languages_full = $matches[1]; // ['en-us', 'fr-ch', 'kok-in']
$accept_languages_short = $matches[2]; // ['en', 'fr', 'kok']
if (!count($accept_languages_full))
{
return false;
}
// if the quality value is absent for an language, use 1 as the default
$q_values = $matches[3]; // ['0.9', '', '0.7']
foreach ($q_values as $i => $q_value)
{
$q_values[$i] = ($q_values[$i] === '') ? 1 : floatval($q_values[$i]);
}
// since quick sort is not stable,
// sort by $indices explicitly after sorting by $q_values
$indices = range(1, count($q_values));
array_multisort(
$q_values, SORT_DESC, SORT_NUMERIC,
$indices, SORT_ASC, SORT_NUMERIC,
$accept_languages_full,
$accept_languages_short
);
// list all enabled language codes in the Piwigo installation
// in both full and short forms, and case insensitive
$languages_available = array();
foreach (get_languages() as $language_code => $language_name)
{
$lowercase_full = strtolower($language_code);
$lowercase_parts = explode('_', $lowercase_full, 2);
$lowercase_prefix = $lowercase_parts[0];
$languages_available[$lowercase_full] = $language_code;
$languages_available[$lowercase_prefix] = $language_code;
}
foreach ($q_values as $i => $q_value)
{
// if the exact language variant is present, make sure it's chosen
// en-US;q=0.9 => en_us => en_US
if (array_key_exists($accept_languages_full[$i], $languages_available))
{
return $languages_available[$accept_languages_full[$i]];
}
// only in case that an exact match was not available,
// should we fallback to other variants in the same language family
// fr_CH => fr => fr_FR
else if (array_key_exists($accept_languages_short[$i], $languages_available))
{
return $languages_available[$accept_languages_short[$i]];
}
}
return false;
}
/**
* Creates user informations based on default values.
*
* @param int|int[] $user_ids
* @param array $override_values values used to override default user values
*/
function create_user_infos($user_ids, $override_values=null)
{
global $conf;
if (!is_array($user_ids))
{
$user_ids = array($user_ids);
}
if (!empty($user_ids))
{
$inserts = array();
list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW();'));
$default_user = get_default_user_info(false);
if ($default_user === false)
{
// Default on structure are used
$default_user = array();
}
if (!is_null($override_values))
{
$default_user = array_merge($default_user, $override_values);
}
foreach ($user_ids as $user_id)
{
$level= isset($default_user['level']) ? $default_user['level'] : 0;
if ($user_id == $conf['webmaster_id'])
{
$status = 'webmaster';
$level = max( $conf['available_permission_levels'] );
}
elseif (($user_id == $conf['guest_id']) or
($user_id == $conf['default_user_id']))
{
$status = 'guest';
}
else
{
$status = 'normal';
}
$insert = array_merge(
array_map('pwg_db_real_escape_string', $default_user),
array(
'user_id' => $user_id,
'status' => $status,
'registration_date' => $dbnow,
'level' => $level
));
$inserts[] = $insert;
}
mass_inserts(USER_INFOS_TABLE, array_keys($inserts[0]), $inserts);
}
}
/**
* Returns the auto login key for an user or false if the user is not found.
*
* @param int $user_id
* @param int $time
* @param string &$username fille with corresponding username
* @return string|false
*/
function calculate_auto_login_key($user_id, $time, &$username)
{
global $conf;
$query = '
SELECT '.$conf['user_fields']['username'].' AS username
, '.$conf['user_fields']['password'].' AS password
FROM '.USERS_TABLE.'
WHERE '.$conf['user_fields']['id'].' = '.$user_id;
$result = pwg_query($query);
if (pwg_db_num_rows($result) > 0)
{
$row = pwg_db_fetch_assoc($result);
$username = stripslashes($row['username']);
$data = $time.$user_id.$username;
$key = base64_encode( hash_hmac('sha1', $data, $conf['secret_key'].$row['password'],true) );
return $key;
}
return false;
}
/**
* Performs all required actions for user login.
*
* @param int $user_id
* @param bool $remember_me
*/
function log_user($user_id, $remember_me)
{
global $conf, $user;
//New default login and register pages, if users changes languages and succesfully logs in
//we want to update the userpref language stored in a cookie
//TODO check value of cookie
if (isset($_COOKIE['lang']) and $user['language'] != $_COOKIE['lang'])
{
if (!array_key_exists($_COOKIE['lang'], get_languages()))
{
fatal_error('[Hacking attempt] the input parameter "'.$_COOKIE['lang'].'" is not valid');
}
single_update(
USER_INFOS_TABLE,
array('language' => $_COOKIE['lang']),
array('user_id' => $user_id)
);
// We unset the lang cookie, if user has changed their language using interface we don't want to keep setting it back
// to what was chosen using standard pages lang switch
setcookie("lang", "", time() - 3600);
}
if ($remember_me and $conf['authorize_remembering'])
{
$now = time();
$key = calculate_auto_login_key($user_id, $now, $username);
if ($key!==false)
{
$cookie = $user_id.'-'.$now.'-'.$key;
setcookie($conf['remember_me_name'],
$cookie,
time()+$conf['remember_me_length'],
cookie_path(),ini_get('session.cookie_domain'),ini_get('session.cookie_secure'),
ini_get('session.cookie_httponly')
);
}
}
else
{ // make sure we clean any remember me ...
setcookie($conf['remember_me_name'], '', 0, cookie_path(),ini_get('session.cookie_domain'));
}
if ( session_id()!="" )
{ // we regenerate the session for security reasons
// see http://www.acros.si/papers/session_fixation.pdf
session_regenerate_id(true);
}
else
{
session_start();
}
$_SESSION['pwg_uid'] = (int)$user_id;
$user['id'] = $_SESSION['pwg_uid'];
trigger_notify('user_login', $user['id']);
pwg_activity('user', $user['id'], 'login');
}
/**
* Performs auto-connection when cookie remember_me exists.
*
* @return bool
*/
function auto_login()
{
global $conf;
if ( isset( $_COOKIE[$conf['remember_me_name']] ) )
{
$cookie = explode('-', stripslashes($_COOKIE[$conf['remember_me_name']]));
if ( count($cookie)===3
and is_numeric(@$cookie[0]) /*user id*/
and is_numeric(@$cookie[1]) /*time*/
and time()-$conf['remember_me_length']<=@$cookie[1]
and time()>=@$cookie[1] /*cookie generated in the past*/ )
{
$key = calculate_auto_login_key( $cookie[0], $cookie[1], $username );
if ($key!==false and $key===$cookie[2])
{
// Since Piwigo 16, 'connected_with' in the session defines the authentication context (UI, API, etc).
// Auto-login via remember-me may miss this, so we set it to 'pwg_ui' for UI logins (not API).
if (script_basename() != 'ws')
{
$_SESSION['connected_with'] = 'pwg_ui';
}
log_user($cookie[0], true);
trigger_notify('login_success', stripslashes($username));
return true;
}
}
setcookie($conf['remember_me_name'], '', 0, cookie_path(),ini_get('session.cookie_domain'));
}
return false;
}
/**
* Hashes a password with the PasswordHash class from phpass security library.
* @since 2.5
*
* @param string $password plain text
* @return string
*/
function pwg_password_hash($password)
{
global $pwg_hasher;
if (empty($pwg_hasher))
{
require_once(PHPWG_ROOT_PATH.'include/passwordhash.class.php');
// We use the portable hash feature from phpass because we can't be sure
// Piwigo runs on PHP 5.3+ (and won't run on an older version in the
// future)
$pwg_hasher = new PasswordHash(13, true);
}
return $pwg_hasher->HashPassword($password);
}
/**
* Verifies a password, with the PasswordHash class from phpass security library.
* If the hash is 'old' (assumed MD5) the hash is updated in database, used for
* migration from Piwigo 2.4.
* @since 2.5
*
* @param string $password plain text
* @param string $hash may be md5 or phpass hashed password
* @param integer $user_id only useful to update password hash from md5 to phpass
* @return bool
*/
function pwg_password_verify($password, $hash, $user_id=null)
{
global $conf, $pwg_hasher;
// If the password has not been hashed with the current algorithm.
if (strpos($hash, '$P') !== 0)
{
if (!empty($conf['pass_convert']))
{
$check = ($hash == $conf['pass_convert']($password));
}
else
{
$check = ($hash == md5($password));
}
if ($check)
{
if (!isset($user_id) or $conf['external_authentification'])
{
return true;
}
// Rehash using new hash.
$hash = pwg_password_hash($password);
single_update(
USERS_TABLE,
array('password' => $hash),
array('id' => $user_id)
);
}
}
// If the stored hash is longer than an MD5, presume the
// new style phpass portable hash.
if (empty($pwg_hasher))
{
require_once(PHPWG_ROOT_PATH.'include/passwordhash.class.php');
// We use the portable hash feature
$pwg_hasher = new PasswordHash(13, true);
}
return $pwg_hasher->CheckPassword($password, $hash);
}
/**
* Tries to login a user given username and password (must be MySql escaped).
*
* @param string $username
* @param string $password
* @param bool $remember_me
* @return bool
*/
function try_log_user($username, $password, $remember_me)
{
return trigger_change('try_log_user', false, $username, $password, $remember_me);
}
add_event_handler('try_log_user', 'pwg_login');
/**
* Default method for user login, can be overwritten with 'try_log_user' trigger.
* @see try_log_user()
*
* @param string $username
* @param string $password
* @param bool $remember_me
* @return bool
*/
function pwg_login($success, $username, $password, $remember_me)
{
if ($success===true)
{
return true;
}
// we force the session table to be clean
pwg_session_gc();
global $conf;
// Find user by username or email (if it exists)
$user_found = find_user_by_username_or_email($username);
// SECURITY: Constant-time authentication to prevent timing attacks
//
// We always perform password verification, even when the user doesn't exist,
// to prevent attackers from distinguishing between:
// - "user exists, wrong password" (slow: runs password_verify)
// - "user doesn't exist" (fast: would skip verification)
//
// This timing difference could allow user enumeration. By using a fake user
// with a pre-generated hash, we ensure consistent execution time regardless
// of whether the account exists or not.
$fake_user = generate_fake_user();
// Verify password with fallback to fake user
$password_verify = $conf['password_verify'](
$password,
$user_found['password'] ?? $fake_user['password'],
$user_found['id'] ?? $fake_user['id']
);
// If the user was not found, is a guest, or the password is incorrect
if (empty($user_found) || 'guest' === $user_found['status'] || !$password_verify)
{
if (!empty($user_found) && !$password_verify)
{
pwg_activity('user', $user_found['id'], 'login_failure_wrong_password');
}
trigger_notify('login_failure', stripslashes($username));
return false;
}
// PLUGIN HOOK: Allow plugins to intercept authentication before log_user()
//
// Expected $state array structure:
// - 'can_login' (bool): Set to false to block login
// - 'reason' (string|null): Custom activity log reason if login blocked
// - 'authenticated' (bool): Set to true if plugin handles log_user() itself
//
// Example plugin implementation:
// add_event_handler('finalize_login', 'my_2fa_check');
// function my_2fa_check($state, $user, $remember_me) {
// if (!verify_2fa_code()) {
// $state['can_login'] = false;
// $state['reason'] = '2fa_failed';
// }
// return $state;
// }
$state = array(
'can_login' => true,
'reason' => null,
'authenticated' => false,
);
$state = trigger_change('finalize_login', $state, $user_found, $remember_me);
if (!$state['can_login'])
{
pwg_activity('user', $user_found['id'], $state['reason'] ?? 'login_failure_before_log_user');
trigger_notify('login_failure_before_log_user', stripslashes($username));
return false;
}
// If plugin handled authentication, skip log_user()
if (!$state['authenticated'])
{
log_user($user_found['id'], $remember_me);
}
clear_fake_user_cache();
trigger_notify('login_success', stripslashes($username));
return true;
}
/**
* Find user by username or email
* search by username first then email
*
* @since 16
* @param string $username_or_email
* @return array|null
*/
function find_user_by_username_or_email($username_or_email)
{
global $conf;
$username_or_email = pwg_db_real_escape_string($username_or_email);
$query = '
SELECT
'.$conf['user_fields']['id'].' AS id,
'.$conf['user_fields']['username'].' AS username,
'.$conf['user_fields']['email'].' AS email,
'.$conf['user_fields']['password'].' AS password,
status
FROM '.USERS_TABLE.' AS u
LEFT JOIN '.USER_INFOS_TABLE.' AS i
ON u.'.$conf['user_fields']['id'].' = i.user_id
WHERE ';
$where_username = $conf['user_fields']['username'].' = \'' . $username_or_email . '\'';
$where_email = $conf['user_fields']['email'].' = \'' . $username_or_email . '\'';
$user = pwg_db_fetch_assoc(pwg_query($query.$where_username))
?: pwg_db_fetch_assoc(pwg_query($query.$where_email));
if (!empty($user))
{
// The user may not exist in the user_infos table, so we consider it's a "normal" user by default
$user['status'] = $user['status'] ?? 'normal';
return $user;
}
return null;
}
/**
* Generate a fake user with hashed password (with the current algo)
*
* SECURITY: This function is used for timing attack mitigation in pwg_login().
* The fake user hash is cached per session to avoid repeated hashing overhead
* while maintaining constant-time authentication behavior.
*
* @since 16
* @return array id and password
*/
function generate_fake_user()
{
global $conf;
// Check if password_hash or password_verify has been changed
$is_verify_hash_changed = 'pwg_password_hash' !== $conf['password_hash']
|| 'pwg_password_verify' !== $conf['password_verify'];
// Generate once per session to avoid repeated hashing overhead.
// Uses current password_hash algorithm to match real user verification costs.
if (!isset($_SESSION['fake_user_cache']) || $is_verify_hash_changed)
{
$fake_password = bin2hex(random_bytes(10));
$_SESSION['fake_user_cache'] = array(
'id' => null,
'password' => $conf['password_hash']($fake_password)
);
}
return $_SESSION['fake_user_cache'];
}
/**
* Clear current session fake user cache
*
* @since 16
* @return void
*/
function clear_fake_user_cache()
{
unset($_SESSION['fake_user_cache']);
}
/**
* Performs all the cleanup on user logout.
*/
function logout_user()
{
global $conf;
trigger_notify('user_logout', @$_SESSION['pwg_uid']);
pwg_activity('user', @$_SESSION['pwg_uid'], 'logout');
$_SESSION = array();
session_unset();
session_destroy();
setcookie(session_name(),'',0,
ini_get('session.cookie_path'),
ini_get('session.cookie_domain')
);
setcookie($conf['remember_me_name'], '', 0, cookie_path(),ini_get('session.cookie_domain'));
}
/**
* Return user status.
*
* @param string $user_status used if $user not initialized
* @return string
*/
function get_user_status($user_status='')
{
global $user;
if (empty($user_status))
{
if (isset($user['status']))
{
$user_status = $user['status'];
}
else
{
// swicth to default value
$user_status = '';
}
}
return $user_status;
}
/**
* Return ACCESS_* value for a given $status.
*
* @param string $user_status used if $user not initialized
* @return int one of ACCESS_* constants
*/
function get_access_type_status($user_status='')
{
global $conf;
switch (get_user_status($user_status))
{
case 'guest':
{
$access_type_status =
($conf['guest_access'] ? ACCESS_GUEST : ACCESS_FREE);
break;
}
case 'generic':
{
$access_type_status = ACCESS_GUEST;
break;
}
case 'normal':
{
$access_type_status = ACCESS_CLASSIC;
break;
}
case 'admin':
{
$access_type_status = ACCESS_ADMINISTRATOR;
break;
}
case 'webmaster':
{
$access_type_status = ACCESS_WEBMASTER;
break;
}
default:
{
$access_type_status = ACCESS_FREE;
break;
}
}
return $access_type_status;
}
/**
* Returns if user has access to a particular ACCESS_*
*
* @return int $access_type one of ACCESS_* constants
* @param string $user_status used if $user not initialized
* @return bool
*/
function is_autorize_status($access_type, $user_status='')
{
return (get_access_type_status($user_status) >= $access_type);
}
/**
* Abord script if user has no access to a particular ACCESS_*
*
* @return int $access_type one of ACCESS_* constants
* @param string $user_status used if $user not initialized
*/
function check_status($access_type, $user_status='')
{
if (!is_autorize_status($access_type, $user_status))
{
access_denied();
}
}
/**
* Returns if user is generic.
*
* @param string $user_status used if $user not initialized
* @return bool
*/
function is_generic($user_status='')
{
return get_user_status($user_status) == 'generic';
}
/**
* Returns if user is a guest.
*
* @param string $user_status used if $user not initialized
* @return bool
*/
function is_a_guest($user_status='')
{
return get_user_status($user_status) == 'guest';
}
/**
* Returns if user is, at least, a classic user.
*
* @param string $user_status used if $user not initialized
* @return bool
*/
function is_classic_user($user_status='')
{
return is_autorize_status(ACCESS_CLASSIC, $user_status);
}
/**
* Returns if user is, at least, an administrator.
*
* @param string $user_status used if $user not initialized
* @return bool
*/
function is_admin($user_status='')
{
return is_autorize_status(ACCESS_ADMINISTRATOR, $user_status);
}
/**
* Returns if user is a webmaster.
*
* @param string $user_status used if $user not initialized
* @return bool
*/
function is_webmaster($user_status='')
{
return is_autorize_status(ACCESS_WEBMASTER, $user_status);
}
/**
* Returns if current user can edit/delete/validate a comment.
*
* @param string $action edit/delete/validate
* @param int $comment_author_id
* @return bool
*/
function can_manage_comment($action, $comment_author_id)
{
global $user, $conf;
if (is_a_guest())
{
return false;
}
if (!in_array($action, array('delete','edit', 'validate')))
{
return false;
}
if (is_admin())
{
return true;
}
if ('edit' == $action and $conf['user_can_edit_comment'])
{
if ($comment_author_id == $user['id']) {
return true;
}
}
if ('delete' == $action and $conf['user_can_delete_comment'])
{
if ($comment_author_id == $user['id']) {
return true;
}
}
return false;
}
/**
* Compute sql WHERE condition with restrict and filter data.
* "FandF" means Forbidden and Filters.
*
* @param array $condition_fields one witch fields apply each filter
* - forbidden_categories
* - visible_categories
* - forbidden_images
* - visible_images
* @param string $prefix_condition prefixes query if condition is not empty
* @param boolean $force_one_condition use at least "1 = 1"
* @return string
*/
function get_sql_condition_FandF(
$condition_fields,
$prefix_condition = null,
$force_one_condition = false
)
{
global $user, $filter;
$sql_list = array();
foreach ($condition_fields as $condition => $field_name)
{
switch($condition)
{
case 'forbidden_categories':
{
if (!empty($user['forbidden_categories']))
{
$sql_list[] =
$field_name.' NOT IN ('.$user['forbidden_categories'].')';
}
break;
}
case 'visible_categories':
{
if (!empty($filter['visible_categories']))
{
$sql_list[] =
$field_name.' IN ('.$filter['visible_categories'].')';
}
break;
}
case 'visible_images':
if (!empty($filter['visible_images']))
{
$sql_list[] =
$field_name.' IN ('.$filter['visible_images'].')';
}
// note there is no break - visible include forbidden
case 'forbidden_images':
if (
!empty($user['image_access_list'])
or $user['image_access_type']!='NOT IN'
)
{
$table_prefix=null;
if ($field_name=='id')
{
$table_prefix = '';
}
elseif ($field_name=='i.id')
{
$table_prefix = 'i.';
}
if ( isset($table_prefix) )
{
$sql_list[]=$table_prefix.'level<='.$user['level'];
}
elseif ( !empty($user['image_access_list']) and !empty($user['image_access_type']) )
{
$sql_list[]=$field_name.' '.$user['image_access_type']
.' ('.$user['image_access_list'].')';
}
}
break;
default:
{
die('Unknow condition');
break;
}
}
}
if (count($sql_list) > 0)
{
$sql = '('.implode(' AND ', $sql_list).')';
}
else
{
$sql = $force_one_condition ? '1 = 1' : '';
}
if (isset($prefix_condition) and !empty($sql))
{
$sql = $prefix_condition.' '.$sql;
}
return $sql;
}
/**
* Returns sql WHERE condition for recent photos/albums for current user.
*
* @param string $db_field
* @return string
*/
function get_recent_photos_sql($db_field)
{
global $user;
if (!isset($user['last_photo_date']))
{
return '0=1';
}
return $db_field.'>=LEAST('
.pwg_db_get_recent_period_expression($user['recent_period'])
.','.pwg_db_get_recent_period_expression(1,$user['last_photo_date']).')';
}
/**
* Performs auto-connection if authentication key is valid.
*
* @since 2.8
*
* @return bool
*/
function auth_key_login($auth_key, $connection_by_header=false)
{
global $conf, $user, $page;
$valid_key = false;
$secret_key = null;
if (preg_match('/^[a-z0-9]{30}$/i', $auth_key))
{
$valid_key = 'auth_key';
}
else if (preg_match('/^pkid-\d{8}-[a-z0-9]{20}:[a-z0-9]{40}$/i', $auth_key))
{
$valid_key = 'api_key';
$tmp_key = explode(':', $auth_key);
$auth_key = $tmp_key[0];
$secret_key = $tmp_key[1];
}
if (!$valid_key) return false;
$query = '
SELECT
*,
'.$conf['user_fields']['username'].' AS username,
'.$conf['user_fields']['email'].' AS email,
NOW() AS dbnow,
DATEDIFF(uak.expired_on, NOW()) AS days_left,
SUBDATE(NOW(), INTERVAL 48 HOUR) AS 48h_ago
FROM '.USER_AUTH_KEYS_TABLE.' AS uak
JOIN '.USER_INFOS_TABLE.' AS ui ON uak.user_id = ui.user_id
JOIN '.USERS_TABLE.' AS u ON u.'.$conf['user_fields']['id'].' = ui.user_id
WHERE auth_key = \''.$auth_key.'\'
;';
$keys = query2array($query);
if (count($keys) == 0)
{
return false;
}
$key = $keys[0];
// is the key still valid?
if (strtotime($key['expired_on']) < strtotime($key['dbnow']))
{
$page['auth_key_invalid'] = true;
return false;
}
// admin/webmaster/guest can't get connected with authentication keys
if ('auth_key' === $valid_key and !in_array($key['status'], array('normal','generic')))
{
return false;
}
// the key is an api_key
if ('api_key' === $valid_key)
{
// check secret
if (!pwg_password_verify($secret_key, $key['apikey_secret']))
{
return false;
}
// is the key is revoked?
if (null != $key['revoked_on'])
{
return false;
}
// check if we need to notificate the user
$days_left = intval($key['days_left']);
if (
$days_left <= 7 // the key expire in max 7 days
and !empty($key['email']) // the user have an email
and (
null === $key['last_notified_on'] // we never send an email for this key
or strtotime($key['last_notified_on']) < strtotime($key['48h_ago']) // OR when the last email was sent more than 48 hours ago
)
)
{
$page['notify_api_key_expiration'] = array(
'days_left' => $days_left,
'dbnow' => $key['dbnow'],
'auth_key' => $key['auth_key']
);
}
}
$user['id'] = $key['user_id'];
// update last used key
single_update(
USER_AUTH_KEYS_TABLE,
array('last_used_on' => $key['dbnow']),
array(
'user_id' => $user['id'],
'auth_key' => $key['auth_key']
),
);
// set the type of connection
$_SESSION['connected_with'] = $valid_key;
// if the connection is made via an API key in the header,
// access is authenticated without creating a persistent user session
// this enables stateless authentication for API calls
if ($connection_by_header)
{
return true;
}
log_user($user['id'], false);
trigger_notify('login_success', $key['username']);
// to be registered in history table by pwg_log function
$page['auth_key_id'] = $key['auth_key_id'];
return true;
}
/**
* Creates an authentication key.
*
* @since 2.8
* @param int $user_id
* @return array
*/
function create_user_auth_key($user_id, $user_status=null)
{
global $conf;
if (0 == $conf['auth_key_duration'])
{
return false;
}
if (!isset($user_status))
{
// we have to find the user status
$query = '
SELECT
status
FROM '.USER_INFOS_TABLE.'
WHERE user_id = '.$user_id.'
;';
$user_infos = query2array($query);
if (count($user_infos) == 0)
{
return false;
}
$user_status = $user_infos[0]['status'];
}
if (!in_array($user_status, array('normal','generic')))
{
return false;
}
$candidate = generate_key(30);
$query = '
SELECT
COUNT(*),
NOW(),
ADDDATE(NOW(), INTERVAL '.$conf['auth_key_duration'].' SECOND)
FROM '.USER_AUTH_KEYS_TABLE.'
WHERE auth_key = \''.$candidate.'\'
;';
list($counter, $now, $expiration) = pwg_db_fetch_row(pwg_query($query));
if (0 == $counter)
{
$key = array(
'auth_key' => $candidate,
'user_id' => $user_id,
'created_on' => $now,
'duration' => $conf['auth_key_duration'],
'expired_on' => $expiration,
'key_type' => 'auth_key',
);
single_insert(USER_AUTH_KEYS_TABLE, $key);
$key['auth_key_id'] = pwg_db_insert_id();
return $key;
}
else
{
return create_user_auth_key($user_id, $user_status);
}
}
/**
* Deactivates authentication keys
*
* @since 2.8
* @param int $user_id
* @return null
*/
function deactivate_user_auth_keys($user_id)
{
$query = '
UPDATE '.USER_AUTH_KEYS_TABLE.'
SET expired_on = NOW()
WHERE user_id = '.$user_id.'
AND expired_on > NOW()
AND key_type = \'auth_key\'
;';
pwg_query($query);
}
/**
* Deactivates password reset key
*
* @since 11
* @param int $user_id
* @return null
*/
function deactivate_password_reset_key($user_id)
{
single_update(
USER_INFOS_TABLE,
array(
'activation_key' => null,
'activation_key_expire' => null,
),
array('user_id' => $user_id)
);
}
/**
* Generate reset password link
*
* @since 15
* @param int $user_id
* @param boolean $first_login
* @return array time_validation and password link
*/
function generate_password_link($user_id, $first_login=false)
{
global $conf;
$activation_key = generate_key(20);
$duration = $first_login
? $conf['password_activation_duration']
: $conf['password_reset_duration'];
list($expire) = pwg_db_fetch_row(pwg_query('SELECT ADDDATE(NOW(), INTERVAL '. $duration .' SECOND)'));
single_update(
USER_INFOS_TABLE,
array(
'activation_key' => pwg_password_hash($activation_key),
'activation_key_expire' => $expire,
),
array('user_id' => $user_id)
);
set_make_full_url();
$password_link = get_root_url().'password.php?key='.$activation_key;
unset_make_full_url();
$time_validation = time_since(
strtotime('now -'.$duration.' second'),
'second',
null,
false
);
return array(
'time_validation' => $time_validation,
'password_link' => $password_link,
);
}
/**
* Gets the last visit (datetime) of a user, based on history table
*
* @since 2.9
* @param int $user_id
* @param boolean $save_in_user_infos to store result in user_infos.last_visit
* @return string date & time of last visit
*/
function get_user_last_visit_from_history($user_id, $save_in_user_infos=false)
{
$last_visit = null;
$query = '
SELECT
date,
time
FROM '.HISTORY_TABLE.'
WHERE user_id = '.$user_id.'
ORDER BY id DESC
LIMIT 1
;';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
$last_visit = $row['date'].' '.$row['time'];
}
if ($save_in_user_infos)
{
$query = '
UPDATE '.USER_INFOS_TABLE.'
SET last_visit = '.(is_null($last_visit) ? 'NULL' : "'".$last_visit."'").',
last_visit_from_history = \'true\',
lastmodified = lastmodified
WHERE user_id = '.$user_id.'
';
pwg_query($query);
}
return $last_visit;
}
/**
* Save user preferences in database
* @since 13
*/
function userprefs_save()
{
global $user;
$dbValue = pwg_db_real_escape_string(serialize($user['preferences']));
$query = '
UPDATE '.USER_INFOS_TABLE.'
SET preferences = \''.$dbValue.'\'
WHERE user_id = '.$user['id'].'
;';
pwg_query($query);
}
/**
* Add or update a user preferences parameter
* @since 13
*
* @param string $param
* @param string $value
* @param boolean $updateGlobal update global *$conf* variable
*/
function userprefs_update_param($param, $value)
{
global $user;
// If the field is true or false, the variable is transformed into a boolean value.
if ('true' == $value)
{
$value = true;
}
elseif ('false' == $value)
{
$value = false;
}
$user['preferences'][$param] = $value;
userprefs_save();
}
/**
* Delete one or more user preferences parameters
* @since 13
*
* @param string|string[] $params
*/
function userprefs_delete_param($params)
{
global $user;
if (!is_array($params))
{
$params = array($params);
}
if (empty($params))
{
return;
}
foreach ($params as $param)
{
if (isset($user['preferences'][$param]))
{
unset($user['preferences'][$param]);
}
}
userprefs_save();
}
/**
* Return a default value for a user preferences parameter.
* @since 13
*
* @param string $param the configuration value to be extracted (if it exists)
* @param mixed $default_value the default value if it does not exist yet.
*
* @return mixed The configuration value if the variable exists, otherwise the default.
*/
function userprefs_get_param($param, $default_value=null)
{
global $user;
if (isset($user['preferences'][$param]))
{
return $user['preferences'][$param];
}
return $default_value;
}
/**
* See if this is the first time the user has logged on
*
* @since 15
* @param int $user_id
* @return bool true if first connexion else false
*/
function has_already_logged_in($user_id)
{
$query = '
SELECT COUNT(*)
FROM '.ACTIVITY_TABLE.'
WHERE action = \'login\' and performed_by = '.$user_id.'';
list($logged_in) = pwg_db_fetch_row(pwg_query($query));
if ($logged_in > 0)
{
return false;
}
return true;
}
/**
* Check all user infos and save parameters
*
* @since 16
* @param mixed[] $params
* @option string username (optional)
* @option string password (optional)
* @option string email (optional)
* @option string status (optional)
* @option int level (optional)
* @option string language (optional)
* @option string theme (optional)
* @option int nb_image_page (optional)
* @option int recent_period (optional)
* @option bool expand (optional)
* @option bool show_nb_comments (optional)
* @option bool show_nb_hits (optional)
* @option bool enabled_high (optional)
*/
function check_and_save_user_infos($params)
{
if (isset($params['username']) and strlen(str_replace( " ", "", $params['username'])) == 0)
{
// return new PwgError(WS_ERR_INVALID_PARAM, 'Name field must not be empty');
return array(
'error' => array(
'code' => WS_ERR_INVALID_PARAM,
'message' => 'Name field must not be empty'
)
);
}
global $conf, $user, $service;
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$updates = $updates_infos = array();
$update_status = null;
if (count($params['user_id']) == 1)
{
if (get_username($params['user_id'][0]) === false)
{
// return new PwgError(WS_ERR_INVALID_PARAM, 'This user does not exist.');
return array(
'error' => array(
'code' => WS_ERR_INVALID_PARAM,
'message' => 'This user does not exist.'
)
);
}
if (!empty($params['username']))
{
$user_id = get_userid($params['username']);
if ($user_id and $user_id != $params['user_id'][0])
{
// return new PwgError(WS_ERR_INVALID_PARAM, l10n('this login is already used'));
return array(
'error' => array(
'code' => WS_ERR_INVALID_PARAM,
'message' => l10n('this login is already used')
)
);
}
if ($params['username'] != strip_tags($params['username']))
{
// return new PwgError(WS_ERR_INVALID_PARAM, l10n('html tags are not allowed in login'));
return array(
'error' => array(
'code' => WS_ERR_INVALID_PARAM,
'message' => l10n('html tags are not allowed in login')
)
);
}
$updates[ $conf['user_fields']['username'] ] = $params['username'];
}
if (!empty($params['email']))
{
if ( ($error = validate_mail_address($params['user_id'][0], $params['email'])) != '')
{
// return new PwgError(WS_ERR_INVALID_PARAM, $error);
return array(
'error' => array(
'code' => WS_ERR_INVALID_PARAM,
'message' => $error
)
);
}
$updates[ $conf['user_fields']['email'] ] = $params['email'];
}
if (!empty($params['password']))
{
if (!is_webmaster())
{
$password_protected_users = array($conf['guest_id']);
$query = '
SELECT
user_id
FROM '.USER_INFOS_TABLE.'
WHERE status IN (\'webmaster\', \'admin\')
;';
$admin_ids = query2array($query, null, 'user_id');
// we add all admin+webmaster users BUT the user herself
$password_protected_users = array_merge($password_protected_users, array_diff($admin_ids, array($user['id'])));
if (in_array($params['user_id'][0], $password_protected_users))
{
// return new PwgError(403, 'Only webmasters can change password of other "webmaster/admin" users');
return array(
'error' => array(
'code' => 403,
'message' => 'Only webmasters can change password of other "webmaster/admin" users'
)
);
}
}
$updates[ $conf['user_fields']['password'] ] = $conf['password_hash']($params['password']);
}
}
if (!empty($params['status']))
{
if (in_array($params['status'], array('webmaster', 'admin')) and !is_webmaster() )
{
// return new PwgError(403, 'Only webmasters can grant "webmaster/admin" status');
return array(
'error' => array(
'code '=> 403,
'message' => 'Only webmasters can grant "webmaster/admin" status'
)
);
}
if ( !in_array($params['status'], array('guest','generic','normal','admin','webmaster')) )
{
// return new PwgError(WS_ERR_INVALID_PARAM, 'Invalid status');
return array(
'error' => array(
'code' => WS_ERR_INVALID_PARAM,
'message' => 'Invalid status'
)
);
}
$protected_users = array(
$user['id'],
$conf['guest_id'],
$conf['webmaster_id'],
);
// an admin can't change status of other admin/webmaster
if ('admin' == $user['status'])
{
$query = '
SELECT
user_id
FROM '.USER_INFOS_TABLE.'
WHERE status IN (\'webmaster\', \'admin\')
;';
$protected_users = array_merge($protected_users, query2array($query, null, 'user_id'));
}
// status update query is separated from the rest as not applying to the same
// set of users (current, guest and webmaster can't be changed)
$params['user_id_for_status'] = array_diff($params['user_id'], $protected_users);
$update_status = $params['status'];
}
if (!empty($params['level']) or @$params['level']===0)
{
if ( !in_array($params['level'], $conf['available_permission_levels']) )
{
// return new PwgError(WS_ERR_INVALID_PARAM, 'Invalid level');
return array(
'error' => array(
'code' => WS_ERR_INVALID_PARAM,
'message' => 'Invalid level'
)
);
}
$updates_infos['level'] = $params['level'];
}
if (!empty($params['language']))
{
if ( !in_array($params['language'], array_keys(get_languages())) )
{
// return new PwgError(WS_ERR_INVALID_PARAM, 'Invalid language');
return array(
'error' => array(
'code' => WS_ERR_INVALID_PARAM,
'message' => 'Invalid language'
)
);
}
$updates_infos['language'] = $params['language'];
}
if (!empty($params['theme']))
{
if ( !in_array($params['theme'], array_keys(get_pwg_themes())) )
{
// return new PwgError(WS_ERR_INVALID_PARAM, 'Invalid theme');
return array(
'error' => array(
'code' => WS_ERR_INVALID_PARAM,
'message' => 'Invalid theme'
)
);
}
$updates_infos['theme'] = $params['theme'];
}
if (!empty($params['nb_image_page']))
{
$updates_infos['nb_image_page'] = $params['nb_image_page'];
}
if (!empty($params['recent_period']) or @$params['recent_period']===0)
{
$updates_infos['recent_period'] = $params['recent_period'];
}
if (!empty($params['expand']) or @$params['expand']===false)
{
$updates_infos['expand'] = boolean_to_string($params['expand']);
}
if (!empty($params['show_nb_comments']) or @$params['show_nb_comments']===false)
{
$updates_infos['show_nb_comments'] = boolean_to_string($params['show_nb_comments']);
}
if (!empty($params['show_nb_hits']) or @$params['show_nb_hits']===false)
{
$updates_infos['show_nb_hits'] = boolean_to_string($params['show_nb_hits']);
}
if (!empty($params['enabled_high']) or @$params['enabled_high']===false)
{
$updates_infos['enabled_high'] = boolean_to_string($params['enabled_high']);
}
// perform updates
single_update(
USERS_TABLE,
$updates,
array($conf['user_fields']['id'] => $params['user_id'][0])
);
if (isset($updates[ $conf['user_fields']['password'] ]))
{
deactivate_user_auth_keys($params['user_id'][0]);
}
if (isset($updates[ $conf['user_fields']['email'] ]))
{
deactivate_password_reset_key($params['user_id'][0]);
}
if (isset($update_status) and count($params['user_id_for_status']) > 0)
{
$query = '
UPDATE '. USER_INFOS_TABLE .' SET
status = "'. $update_status .'"
WHERE user_id IN('. implode(',', $params['user_id_for_status']) .')
;';
pwg_query($query);
// we delete sessions, ie disconnect, for users if status becomes "guest".
// It's like deactivating the user.
if ('guest' == $update_status)
{
foreach ($params['user_id_for_status'] as $user_id_for_status)
{
delete_user_sessions($user_id_for_status);
}
}
}
if (count($updates_infos) > 0)
{
$query = '
UPDATE '. USER_INFOS_TABLE .' SET ';
$first = true;
foreach ($updates_infos as $field => $value)
{
if (!$first) $query.= ', ';
else $first = false;
$query.= $field .' = "'. $value .'"';
}
$query.= '
WHERE user_id IN('. implode(',', $params['user_id']) .')
;';
pwg_query($query);
}
// manage association to groups
if (!empty($params['group_id']))
{
$query = '
DELETE
FROM '.USER_GROUP_TABLE.'
WHERE user_id IN ('.implode(',', $params['user_id']).')
;';
pwg_query($query);
// we remove all provided groups that do not really exist
$query = '
SELECT
id
FROM `'.GROUPS_TABLE.'`
WHERE id IN ('.implode(',', $params['group_id']).')
;';
$group_ids = array_from_query($query, 'id');
// if only -1 (a group id that can't exist) is in the list, then no
// group is associated
if (count($group_ids) > 0)
{
$inserts = array();
foreach ($group_ids as $group_id)
{
foreach ($params['user_id'] as $user_id)
{
$inserts[] = array('user_id' => $user_id, 'group_id' => $group_id);
}
}
mass_inserts(USER_GROUP_TABLE, array_keys($inserts[0]), $inserts);
}
}
invalidate_user_cache();
pwg_activity('user', $params['user_id'], 'edit');
return array(
'user_id' => $params['user_id'],
'infos' => $updates_infos,
'account' => $updates
);
}
/**
* Create a new api_key
*
* @since 16
* @param int $user_id
* @param int|null $duration
* @param string $key_name
* @return array auth_key / apikey_secret / apikey_name /
* user_id / created_on / duration / expired_on / key_type
*/
function create_api_key($user_id, $duration, $key_name)
{
$key_id = 'pkid-'.date('Ymd').'-'.generate_key(20);
$key_secret = generate_key(40);
list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW();'));
$key = array(
'auth_key' => $key_id,
'apikey_secret' => pwg_password_hash($key_secret),
'apikey_name' => $key_name,
'user_id' => $user_id,
'created_on' => $dbnow,
'key_type' => 'api_key'
);
if (!empty($duration))
{
$query = '
SELECT
ADDDATE(NOW(), INTERVAL '.($duration * 60 * 60 * 24).' SECOND)
;';
list($expiration) = pwg_db_fetch_row(pwg_query($query));
$key['duration'] = $duration;
}
$key['expired_on'] = $expiration;
single_insert(USER_AUTH_KEYS_TABLE, $key);
$key['apikey_secret'] = $key_secret;
return $key;
}
/**
* Revoke a api_key
*
* @since 16
* @param int $user_id
* @param string $pkid
* @return string|bool
*/
function revoke_api_key($user_id, $pkid)
{
$query = '
SELECT
COUNT(*),
NOW()
FROM `'.USER_AUTH_KEYS_TABLE.'`
WHERE auth_key = "'.$pkid.'"
AND user_id = '.$user_id.'
;';
list($key, $now) = pwg_db_fetch_row(pwg_query($query));
if ($key == 0)
{
return l10n('API Key not found');
}
single_update(
USER_AUTH_KEYS_TABLE,
array('revoked_on' => $now),
array(
'auth_key' => $pkid,
'user_id' => $user_id
)
);
return true;
}
/**
* Edit a api_key
*
* @since 16
* @param int $user_id
* @param string $pkid
* @return string|bool
*/
function edit_api_key($user_id, $pkid, $api_name)
{
$query = '
SELECT
COUNT(*)
FROM `'.USER_AUTH_KEYS_TABLE.'`
WHERE auth_key = "'.$pkid.'"
AND user_id = '.$user_id.'
;';
list($key) = pwg_db_fetch_row(pwg_query($query));
if ($key == 0)
{
return l10n('API Key not found');
}
single_update(
USER_AUTH_KEYS_TABLE,
array('apikey_name' => $api_name),
array(
'auth_key' => $pkid,
'user_id' => $user_id
)
);
return true;
}
/**
* Get all api_key
*
* @since 16
* @param string $user_id
* @return array|false
*/
function get_api_key($user_id)
{
$query = '
SELECT *
FROM `'.USER_AUTH_KEYS_TABLE.'`
WHERE user_id = '.$user_id.'
AND key_type = "api_key"
;';
$api_keys = query2array($query);
if (!$api_keys) return false;
$query = '
SELECT
NOW()
;';
list($now) = pwg_db_fetch_row(pwg_query($query));
foreach ($api_keys as $i => $api_key)
{
$api_key['apikey_secret'] = str_repeat("*", 40);
unset($api_key['auth_key_id'], $api_key['user_id'], $api_key['key_type']);
$api_key['apikey_name'] = stripslashes($api_key['apikey_name']);
$api_key['created_on_format'] = format_date($api_key['created_on'], array('day', 'month', 'year'));
$api_key['expired_on_format'] = format_date($api_key['expired_on'], array('day', 'month', 'year'));
$api_key['last_used_on_since'] =
$api_key['last_used_on']
? time_since($api_key['last_used_on'], 'day')
: l10n('Never');
$expired_on = str2DateTime($api_key['expired_on']);
$now = str2DateTime($now);
$api_key['is_expired'] = $expired_on < $now;
if ($api_key['is_expired'])
{
$api_key['expiration'] = l10n('Expired');
}
else
{
$diff = dateDiff($now, $expired_on);
if ($diff->days > 0)
{
$api_key['expiration'] = l10n('%d days', $diff->days);
}
elseif ($diff->h > 0)
{
$api_key['expiration'] = l10n('%d hours', $diff->h);
}
else
{
$api_key['expiration'] = l10n('%d minutes', $diff->i);
}
}
$api_key['expired_on_since'] = time_since($api_key['expired_on'], 'day');
$api_key['revoked_on_since'] =
$api_key['revoked_on']
? time_since($api_key['revoked_on'], 'day')
: null;
$api_key['revoked_on_message'] =
$api_key['revoked_on']
? l10n('This API key was manually revoked on %s', format_date($api_key['revoked_on'], array('day', 'month', 'year')))
: null;
$api_keys[$i] = $api_key;
}
return $api_keys;
}
/**
* Get all available api_key
*
* @since 16
* @param string $user_id
* @return array|false
*/
function get_available_api_key($user_id)
{
$api_keys = get_api_key($user_id);
if (!$api_keys) return false;
$available = array();
foreach($api_keys as $api_key)
{
if (!$api_key['is_expired'] && empty($api_key['revoked_on']))
{
$available[] = $api_key;
}
}
return count($available) > 0 ? $available : false;
}
/**
* Is connected with pwg_ui (identification.php)
*
* @since 16
* @return bool
*/
function connected_with_pwg_ui()
{
// You can manage your api key only if you are connected via identification.php
if (isset($_SESSION['connected_with']) and 'pwg_ui' === $_SESSION['connected_with'])
{
return true;
}
return false;
}
/**
* Notify an user when his api key is about to expire
*
* @since 16
* @return bool
*/
function notification_api_key_expiration($username, $email, $days_left)
{
global $conf;
include_once(PHPWG_ROOT_PATH . 'include/functions_mail.inc.php');
$days_left_str = $days_left <= 1 ?
l10n('Your API key will expire in %d day.', $days_left)
: l10n('Your API key will expire in %d days.', $days_left);
$message = '<p style="margin: 20px 0">' . l10n('Hello %s,', $username) . '</p>';
$message .= '<p style="margin: 20px 0">' . $days_left_str . '</p>';
$message .= '<p style="margin: 20px 0">' . l10n('To continue using the API, please renew your key before it expires.') . '</p>';
$message .= '<p style="margin: 20px 0">' . l10n('You can manage your API keys in your <a href="%s">account settings.</a>', get_absolute_root_url().'profile.php') . '</p>';
$result = @pwg_mail(
$email,
array(
'subject' => '[' . $conf['gallery_title'] . '] ' . l10n('Your API key will expire soon'),
'content' => $message,
'content_format' => 'text/html',
)
);
return $result;
}
/**
* Generate an user code for verification
*
* @since 16
* @return array [$secret, $code]
*/
function generate_user_code()
{
global $conf;
require_once(PHPWG_ROOT_PATH . 'include/totp.class.php');
$secret = PwgTOTP::generateSecret();
$code = PwgTOTP::generateCode($secret, min($conf['password_reset_code_duration'], 900)); // max 15 minutes
return array(
'secret' => $secret,
'code' => $code
);
}
/**
* Verify user code
*
* @since 16
* @param string $secret
* @param string $code
* @return bool
*/
function verify_user_code($secret, $code)
{
global $conf;
require_once(PHPWG_ROOT_PATH . 'include/totp.class.php');
return PwgTOTP::verifyCode($code, $secret, min($conf['password_reset_code_duration'], 900), 1);
}
/**
* Register in the user session, the "context" of the last 10 viewed images.
*
* @since 16
*/
function save_edit_context()
{
global $page;
if (!is_admin() or !isset($page['section_url']) or !isset($page['image_id']))
{
return;
}
$_SESSION['edit_context'] ??= [];
// the $page['section_url'] is set in the include/section_init script. It
// contains the URL describing the "context" of the photo. Examples:
//
// * /198/list/2,69,198
// * /198/category/18801-yes_man
// * /198/tags/27-city_nantes/28-city_rennes
// * /198/search/psk-20251103-lqCHHAFSZY/posted-monthly-list-2025-3
//
// same photo #198 in different context. We need it to propose the best
// return page on the photo edit page in the administration.
// let's add the item on top of previous registered values and keep only the last 10 values
$_SESSION['edit_context'] = array_slice(array($page['image_id'] => $page['section_url']) + $_SESSION['edit_context'], 0, 10, true);
}
/**
* Returns the "context" of the requested image.
*
* @since 16
* @param int $image_id
* @return string|bool
*/
function get_edit_context($image_id)
{
if (!isset($_SESSION['edit_context'][$image_id]))
{
return false;
}
return preg_replace('/^\/'.$image_id.'\//', '', $_SESSION['edit_context'][$image_id]);
}
?>