* @license GNU General Public License, version 2 (GPL-2.0) * * For full copyright and license information, please see * the docs/CREDITS.txt file. * */ namespace phpbb; /** * Base user class * * This is the overarching class which contains (through session extend) * all methods utilised for user functionality during a session. */ class user extends \phpbb\session { var $lang = array(); var $help = array(); var $style = array(); var $date_format; /** * DateTimeZone object holding the timezone of the user */ public $timezone; /** * @var string Class name of datetime object */ protected $datetime; var $lang_name = false; var $lang_id = false; var $lang_path; var $img_lang; var $img_array = array(); // Able to add new options (up to id 31) var $keyoptions = array('viewimg' => 0, 'viewflash' => 1, 'viewsmilies' => 2, 'viewsigs' => 3, 'viewavatars' => 4, 'viewcensors' => 5, 'attachsig' => 6, 'bbcode' => 8, 'smilies' => 9, 'sig_bbcode' => 15, 'sig_smilies' => 16, 'sig_links' => 17); /** * Constructor to set the lang path * @param string $datetime_class Class name of datetime class */ function __construct($datetime_class) { global $phpbb_root_path; $this->lang_path = $phpbb_root_path . 'language/'; $this->datetime = $datetime_class; } /** * Function to set custom language path (able to use directory outside of phpBB) * * @param string $lang_path New language path used. * @access public */ function set_custom_lang_path($lang_path) { $this->lang_path = $lang_path; if (substr($this->lang_path, -1) != '/') { $this->lang_path .= '/'; } } /** * Setup basic user-specific items (style, language, ...) */ function setup($lang_set = false, $style_id = false) { global $db, $request, $template, $config, $auth, $phpEx, $phpbb_root_path, $cache; global $phpbb_dispatcher; if ($this->data['user_id'] != ANONYMOUS) { $user_lang_name = (file_exists($this->lang_path . $this->data['user_lang'] . "/common.$phpEx")) ? $this->data['user_lang'] : basename($config['default_lang']); $user_date_format = $this->data['user_dateformat']; $user_timezone = $this->data['user_timezone']; } else { $lang_override = $request->variable('language', ''); if ($lang_override) { $this->set_cookie('lang', $lang_override, 0, false); } else { $lang_override = $request->variable($config['cookie_name'] . '_lang', '', true, \phpbb\request\request_interface::COOKIE); } if ($lang_override) { $use_lang = basename($lang_override); $user_lang_name = (file_exists($this->lang_path . $use_lang . "/common.$phpEx")) ? $use_lang : basename($config['default_lang']); $this->data['user_lang'] = $user_lang_name; } else { $user_lang_name = basename($config['default_lang']); } $user_date_format = $config['default_dateformat']; $user_timezone = $config['board_timezone']; /** * If a guest user is surfing, we try to guess his/her language first by obtaining the browser language * If re-enabled we need to make sure only those languages installed are checked * Commented out so we do not loose the code. if ($request->header('Accept-Language')) { $accept_lang_ary = explode(',', $request->header('Accept-Language')); foreach ($accept_lang_ary as $accept_lang) { // Set correct format ... guess full xx_YY form $accept_lang = substr($accept_lang, 0, 2) . '_' . strtoupper(substr($accept_lang, 3, 2)); $accept_lang = basename($accept_lang); if (file_exists($this->lang_path . $accept_lang . "/common.$phpEx")) { $user_lang_name = $config['default_lang'] = $accept_lang; break; } else { // No match on xx_YY so try xx $accept_lang = substr($accept_lang, 0, 2); $accept_lang = basename($accept_lang); if (file_exists($this->lang_path . $accept_lang . "/common.$phpEx")) { $user_lang_name = $config['default_lang'] = $accept_lang; break; } } } } */ } $user_data = $this->data; $lang_set_ext = array(); /** * Event to load language files and modify user data on every page * * @event core.user_setup * @var array user_data Array with user's data row * @var string user_lang_name Basename of the user's langauge * @var string user_date_format User's date/time format * @var string user_timezone User's timezone, should be one of * http://www.php.net/manual/en/timezones.php * @var mixed lang_set String or array of language files * @var array lang_set_ext Array containing entries of format * array( * 'ext_name' => (string) [extension name], * 'lang_set' => (string|array) [language files], * ) * For performance reasons, only load translations * that are absolutely needed globally using this * event. Use local events otherwise. * @var mixed style_id Style we are going to display * @since 3.1.0-a1 */ $vars = array( 'user_data', 'user_lang_name', 'user_date_format', 'user_timezone', 'lang_set', 'lang_set_ext', 'style_id', ); extract($phpbb_dispatcher->trigger_event('core.user_setup', compact($vars))); $this->data = $user_data; $this->lang_name = $user_lang_name; $this->date_format = $user_date_format; try { $this->timezone = new \DateTimeZone($user_timezone); } catch (\Exception $e) { // If the timezone the user has selected is invalid, we fall back to UTC. $this->timezone = new \DateTimeZone('UTC'); } // We include common language file here to not load it every time a custom language file is included $lang = &$this->lang; // Do not suppress error if in DEBUG mode $include_result = (defined('DEBUG')) ? (include $this->lang_path . $this->lang_name . "/common.$phpEx") : (@include $this->lang_path . $this->lang_name . "/common.$phpEx"); if ($include_result === false) { die('Language file ' . $this->lang_path . $this->lang_name . "/common.$phpEx" . " couldn't be opened."); } $this->add_lang($lang_set); unset($lang_set); foreach ($lang_set_ext as $ext_lang_pair) { $this->add_lang_ext($ext_lang_pair['ext_name'], $ext_lang_pair['lang_set']); } unset($lang_set_ext); $style_request = $request->variable('style', 0); if ($style_request && (!$config['override_user_style'] || $auth->acl_get('a_styles')) && !defined('ADMIN_START')) { global $SID, $_EXTRA_URL; $style_id = $style_request; $SID .= '&style=' . $style_id; $_EXTRA_URL = array('style=' . $style_id); } else { // Set up style $style_id = ($style_id) ? $style_id : ((!$config['override_user_style']) ? $this->data['user_style'] : $config['default_style']); } $sql = 'SELECT * FROM ' . STYLES_TABLE . " s WHERE s.style_id = $style_id"; $result = $db->sql_query($sql, 3600); $this->style = $db->sql_fetchrow($result); $db->sql_freeresult($result); // Fallback to user's standard style if (!$this->style && $style_id != $this->data['user_style']) { $style_id = $this->data['user_style']; $sql = 'SELECT * FROM ' . STYLES_TABLE . " s WHERE s.style_id = $style_id"; $result = $db->sql_query($sql, 3600); $this->style = $db->sql_fetchrow($result); $db->sql_freeresult($result); } // User has wrong style if (!$this->style && $style_id == $this->data['user_style']) { $style_id = $this->data['user_style'] = $config['default_style']; $sql = 'UPDATE ' . USERS_TABLE . " SET user_style = $style_id WHERE user_id = {$this->data['user_id']}"; $db->sql_query($sql); $sql = 'SELECT * FROM ' . STYLES_TABLE . " s WHERE s.style_id = $style_id"; $result = $db->sql_query($sql, 3600); $this->style = $db->sql_fetchrow($result); $db->sql_freeresult($result); } if (!$this->style) { trigger_error('NO_STYLE_DATA', E_USER_ERROR); } // Now parse the cfg file and cache it $parsed_items = $cache->obtain_cfg_items($this->style); $check_for = array( 'pagination_sep' => (string) ', ' ); foreach ($check_for as $key => $default_value) { $this->style[$key] = (isset($parsed_items[$key])) ? $parsed_items[$key] : $default_value; settype($this->style[$key], gettype($default_value)); if (is_string($default_value)) { $this->style[$key] = htmlspecialchars($this->style[$key]); } } $template->set_style(); $this->img_lang = $this->lang_name; // Call phpbb_user_session_handler() in case external application want to "bend" some variables or replace classes... // After calling it we continue script execution... phpbb_user_session_handler(); // If this function got called from the error handler we are finished here. if (defined('IN_ERROR_HANDLER')) { return; } // Disable board if the install/ directory is still present // For the brave development army we do not care about this, else we need to comment out this everytime we develop locally if (!defined('DEBUG') && !defined('ADMIN_START') && !defined('IN_INSTALL') && !defined('IN_LOGIN') && file_exists($phpbb_root_path . 'install') && !is_file($phpbb_root_path . 'install')) { // Adjust the message slightly according to the permissions if ($auth->acl_gets('a_', 'm_') || $auth->acl_getf_global('m_')) { $message = 'REMOVE_INSTALL'; } else { $message = (!empty($config['board_disable_msg'])) ? $config['board_disable_msg'] : 'BOARD_DISABLE'; } trigger_error($message); } // Is board disabled and user not an admin or moderator? if ($config['board_disable'] && !defined('IN_LOGIN') && !defined('SKIP_CHECK_DISABLED') && !$auth->acl_gets('a_', 'm_') && !$auth->acl_getf_global('m_')) { if ($this->data['is_bot']) { send_status_line(503, 'Service Unavailable'); } $message = (!empty($config['board_disable_msg'])) ? $config['board_disable_msg'] : 'BOARD_DISABLE'; trigger_error($message); } // Is load exceeded? if ($config['limit_load'] && $this->load !== false) { if ($this->load > floatval($config['limit_load']) && !defined('IN_LOGIN') && !defined('IN_ADMIN')) { // Set board disabled to true to let the admins/mods get the proper notification $config['board_disable'] = '1'; if (!$auth->acl_gets('a_', 'm_') && !$auth->acl_getf_global('m_')) { if ($this->data['is_bot']) { send_status_line(503, 'Service Unavailable'); } trigger_error('BOARD_UNAVAILABLE'); } } } if (isset($this->data['session_viewonline'])) { // Make sure the user is able to hide his session if (!$this->data['session_viewonline']) { // Reset online status if not allowed to hide the session... if (!$auth->acl_get('u_hideonline')) { $sql = 'UPDATE ' . SESSIONS_TABLE . ' SET session_viewonline = 1 WHERE session_user_id = ' . $this->data['user_id']; $db->sql_query($sql); $this->data['session_viewonline'] = 1; } } else if (!$this->data['user_allow_viewonline']) { // the user wants to hide and is allowed to -> cloaking device on. if ($auth->acl_get('u_hideonline')) { $sql = 'UPDATE ' . SESSIONS_TABLE . ' SET session_viewonline = 0 WHERE session_user_id = ' . $this->data['user_id']; $db->sql_query($sql); $this->data['session_viewonline'] = 0; } } } // Does the user need to change their password? If so, redirect to the // ucp profile reg_details page ... of course do not redirect if we're already in the ucp if (!defined('IN_ADMIN') && !defined('ADMIN_START') && $config['chg_passforce'] && !empty($this->data['is_registered']) && $auth->acl_get('u_chgpasswd') && $this->data['user_passchg'] < time() - ($config['chg_passforce'] * 86400)) { if (strpos($this->page['query_string'], 'mode=reg_details') === false && $this->page['page_name'] != "ucp.$phpEx") { redirect(append_sid("{$phpbb_root_path}ucp.$phpEx", 'i=profile&mode=reg_details')); } } return; } /** * More advanced language substitution * Function to mimic sprintf() with the possibility of using phpBB's language system to substitute nullar/singular/plural forms. * Params are the language key and the parameters to be substituted. * This function/functionality is inspired by SHS` and Ashe. * * Example call: $user->lang('NUM_POSTS_IN_QUEUE', 1); * * If the first parameter is an array, the elements are used as keys and subkeys to get the language entry: * Example: $user->lang(array('datetime', 'AGO'), 1) uses $user->lang['datetime']['AGO'] as language entry. */ function lang() { $args = func_get_args(); $key = $args[0]; if (is_array($key)) { $lang = &$this->lang[array_shift($key)]; foreach ($key as $_key) { $lang = &$lang[$_key]; } } else { $lang = &$this->lang[$key]; } // Return if language string does not exist if (!isset($lang) || (!is_string($lang) && !is_array($lang))) { return $key; } // If the language entry is a string, we simply mimic sprintf() behaviour if (is_string($lang)) { if (sizeof($args) == 1) { return $lang; } // Replace key with language entry and simply pass along... $args[0] = $lang; return call_user_func_array('sprintf', $args); } else if (sizeof($lang) == 0) { // If the language entry is an empty array, we just return the language key return $args[0]; } // It is an array... now handle different nullar/singular/plural forms $key_found = false; // We now get the first number passed and will select the key based upon this number for ($i = 1, $num_args = sizeof($args); $i < $num_args; $i++) { if (is_int($args[$i]) || is_float($args[$i])) { if ($args[$i] == 0 && isset($lang[0])) { // We allow each translation using plural forms to specify a version for the case of 0 things, // so that "0 users" may be displayed as "No users". $key_found = 0; break; } else { $use_plural_form = $this->get_plural_form($args[$i]); if (isset($lang[$use_plural_form])) { // The key we should use exists, so we use it. $key_found = $use_plural_form; } else { // If the key we need to use does not exist, we fall back to the previous one. $numbers = array_keys($lang); foreach ($numbers as $num) { if ($num > $use_plural_form) { break; } $key_found = $num; } } break; } } } // Ok, let's check if the key was found, else use the last entry (because it is mostly the plural form) if ($key_found === false) { $numbers = array_keys($lang); $key_found = end($numbers); } // Use the language string we determined and pass it to sprintf() $args[0] = $lang[$key_found]; return call_user_func_array('sprintf', $args); } /** * Determine which plural form we should use. * For some languages this is not as simple as for English. * * @param $number int|float The number we want to get the plural case for. Float numbers are floored. * @param $force_rule mixed False to use the plural rule of the language package * or an integer to force a certain plural rule * @return int The plural-case we need to use for the number plural-rule combination */ function get_plural_form($number, $force_rule = false) { $number = (int) $number; // Default to English system $plural_rule = ($force_rule !== false) ? $force_rule : ((isset($this->lang['PLURAL_RULE'])) ? $this->lang['PLURAL_RULE'] : 1); return phpbb_get_plural_form($plural_rule, $number); } /** * Add Language Items - use_db and use_help are assigned where needed (only use them to force inclusion) * * @param mixed $lang_set specifies the language entries to include * @param bool $use_db internal variable for recursion, do not use * @param bool $use_help internal variable for recursion, do not use * @param string $ext_name The extension to load language from, or empty for core files * * Examples: * * $lang_set = array('posting', 'help' => 'faq'); * $lang_set = array('posting', 'viewtopic', 'help' => array('bbcode', 'faq')) * $lang_set = array(array('posting', 'viewtopic'), 'help' => array('bbcode', 'faq')) * $lang_set = 'posting' * $lang_set = array('help' => 'faq', 'db' => array('help:faq', 'posting')) * */ function add_lang($lang_set, $use_db = false, $use_help = false, $ext_name = '') { global $phpEx; if (is_array($lang_set)) { foreach ($lang_set as $key => $lang_file) { // Please do not delete this line. // We have to force the type here, else [array] language inclusion will not work $key = (string) $key; if ($key == 'db') { $this->add_lang($lang_file, true, $use_help, $ext_name); } else if ($key == 'help') { $this->add_lang($lang_file, $use_db, true, $ext_name); } else if (!is_array($lang_file)) { $this->set_lang($this->lang, $this->help, $lang_file, $use_db, $use_help, $ext_name); } else { $this->add_lang($lang_file, $use_db, $use_help, $ext_name); } } unset($lang_set); } else if ($lang_set) { $this->set_lang($this->lang, $this->help, $lang_set, $use_db, $use_help, $ext_name); } } /** * Add Language Items from an extension - use_db and use_help are assigned where needed (only use them to force inclusion) * * @param string $ext_name The extension to load language from, or empty for core files * @param mixed $lang_set specifies the language entries to include * @param bool $use_db internal variable for recursion, do not use * @param bool $use_help internal variable for recursion, do not use */ function add_lang_ext($ext_name, $lang_set, $use_db = false, $use_help = false) { if ($ext_name === '/') { $ext_name = ''; } $this->add_lang($lang_set, $use_db, $use_help, $ext_name); } /** * Set language entry (called by add_lang) * @access private */ function set_lang(&$lang, &$help, $lang_file, $use_db = false, $use_help = false, $ext_name = '') { global $phpbb_root_path, $phpEx; // Make sure the language name is set (if the user setup did not happen it is not set) if (!$this->lang_name) { global $config; $this->lang_name = basename($config['default_lang']); } // $lang == $this->lang // $help == $this->help // - add appropriate variables here, name them as they are used within the language file... if (!$use_db) { if ($use_help && strpos($lang_file, '/') !== false) { $filename = dirname($lang_file) . '/help_' . basename($lang_file); } else { $filename = (($use_help) ? 'help_' : '') . $lang_file; } if ($ext_name) { global $phpbb_extension_manager; $ext_path = $phpbb_extension_manager->get_extension_path($ext_name, true); $lang_path = $ext_path . 'language/'; } else { $lang_path = $this->lang_path; } if (strpos($phpbb_root_path . $filename, $lang_path . $this->lang_name . '/') === 0) { $language_filename = $phpbb_root_path . $filename; } else { $language_filename = $lang_path . $this->lang_name . '/' . $filename . '.' . $phpEx; } // If we are in install, try to use the updated version, when available $install_language_filename = str_replace('language/', 'install/update/new/language/', $language_filename); if (defined('IN_INSTALL') && file_exists($install_language_filename)) { $language_filename = $install_language_filename; } if (!file_exists($language_filename)) { global $config; if ($this->lang_name == 'en') { // The user's selected language is missing the file, the board default's language is missing the file, and the file doesn't exist in /en. $language_filename = str_replace($lang_path . 'en', $lang_path . $this->data['user_lang'], $language_filename); trigger_error('Language file ' . $language_filename . ' couldn\'t be opened.', E_USER_ERROR); } else if ($this->lang_name == basename($config['default_lang'])) { // Fall back to the English Language $reset_lang_name = $this->lang_name; $this->lang_name = 'en'; $this->set_lang($lang, $help, $lang_file, $use_db, $use_help, $ext_name); $this->lang_name = $reset_lang_name; } else if ($this->lang_name == $this->data['user_lang']) { // Fall back to the board default language $reset_lang_name = $this->lang_name; $this->lang_name = basename($config['default_lang']); $this->set_lang($lang, $help, $lang_file, $use_db, $use_help, $ext_name); $this->lang_name = $reset_lang_name; } return; } // Do not suppress error if in DEBUG mode $include_result = (defined('DEBUG')) ? (include $language_filename) : (@include $language_filename); if ($include_result === false) { trigger_error('Language file ' . $language_filename . ' couldn\'t be opened.', E_USER_ERROR); } } else if ($use_db) { // Get Database Language Strings // Put them into $lang if nothing is prefixed, put them into $help if help: is prefixed // For example: help:faq, posting } } /** * Format user date * * @param int $gmepoch unix timestamp * @param string $format date format in date() notation. | used to indicate relative dates, for example |d m Y|, h:i is translated to Today, h:i. * @param bool $forcedate force non-relative date format. * * @return mixed translated date */ function format_date($gmepoch, $format = false, $forcedate = false) { static $utc; if (!isset($utc)) { $utc = new \DateTimeZone('UTC'); } $time = new $this->datetime($this, "@$gmepoch", $utc); $time->setTimezone($this->timezone); return $time->format($format, $forcedate); } /** * Create a \phpbb\datetime object in the context of the current user * * @since 3.1 * @param string $time String in a format accepted by strtotime(). * @param DateTimeZone $timezone Time zone of the time. * @return \phpbb\datetime Date time object linked to the current users locale */ public function create_datetime($time = 'now', \DateTimeZone $timezone = null) { $timezone = $timezone ?: $this->timezone; return new $this->datetime($this, $time, $timezone); } /** * Get the UNIX timestamp for a datetime in the users timezone, so we can store it in the database. * * @param string $format Format of the entered date/time * @param string $time Date/time with the timezone applied * @param DateTimeZone $timezone Timezone of the date/time, falls back to timezone of current user * @return int Returns the unix timestamp */ public function get_timestamp_from_format($format, $time, \DateTimeZone $timezone = null) { $timezone = $timezone ?: $this->timezone; $date = \DateTime::createFromFormat($format, $time, $timezone); return ($date !== false) ? $date->format('U') : false; } /** * Get language id currently used by the user */ function get_iso_lang_id() { global $config, $db; if (!empty($this->lang_id)) { return $this->lang_id; } if (!$this->lang_name) { $this->lang_name = $config['default_lang']; } $sql = 'SELECT lang_id FROM ' . LANG_TABLE . " WHERE lang_iso = '" . $db->sql_escape($this->lang_name) . "'"; $result = $db->sql_query($sql); $this->lang_id = (int) $db->sql_fetchfield('lang_id'); $db->sql_freeresult($result); return $this->lang_id; } /** * Get users profile fields */ function get_profile_fields($user_id) { global $db; if (isset($this->profile_fields)) { return; } $sql = 'SELECT * FROM ' . PROFILE_FIELDS_DATA_TABLE . " WHERE user_id = $user_id"; $result = $db->sql_query_limit($sql, 1); $this->profile_fields = (!($row = $db->sql_fetchrow($result))) ? array() : $row; $db->sql_freeresult($result); } /** * Specify/Get image */ function img($img, $alt = '') { $title = ''; if ($alt) { $alt = $this->lang($alt); $title = ' title="' . $alt . '"'; } return '' . $alt . ''; } /** * Get option bit field from user options. * * @param int $key option key, as defined in $keyoptions property. * @param int $data bit field value to use, or false to use $this->data['user_options'] * @return bool true if the option is set in the bit field, false otherwise */ function optionget($key, $data = false) { $var = ($data !== false) ? $data : $this->data['user_options']; return phpbb_optionget($this->keyoptions[$key], $var); } /** * Set option bit field for user options. * * @param int $key Option key, as defined in $keyoptions property. * @param bool $value True to set the option, false to clear the option. * @param int $data Current bit field value, or false to use $this->data['user_options'] * @return int|bool If $data is false, the bit field is modified and * written back to $this->data['user_options'], and * return value is true if the bit field changed and * false otherwise. If $data is not false, the new * bitfield value is returned. */ function optionset($key, $value, $data = false) { $var = ($data !== false) ? $data : $this->data['user_options']; $new_var = phpbb_optionset($this->keyoptions[$key], $value, $var); if ($data === false) { if ($new_var != $var) { $this->data['user_options'] = $new_var; return true; } else { return false; } } else { return $new_var; } } /** * Funtion to make the user leave the NEWLY_REGISTERED system group. * @access public */ function leave_newly_registered() { global $db; if (empty($this->data['user_new'])) { return false; } if (!function_exists('remove_newly_registered')) { global $phpbb_root_path, $phpEx; include($phpbb_root_path . 'includes/functions_user.' . $phpEx); } if ($group = remove_newly_registered($this->data['user_id'], $this->data)) { $this->data['group_id'] = $group; } $this->data['user_permissions'] = ''; $this->data['user_new'] = 0; return true; } /** * Returns all password protected forum ids the user is currently NOT authenticated for. * * @return array Array of forum ids * @access public */ function get_passworded_forums() { global $db; $sql = 'SELECT f.forum_id, fa.user_id FROM ' . FORUMS_TABLE . ' f LEFT JOIN ' . FORUMS_ACCESS_TABLE . " fa ON (fa.forum_id = f.forum_id AND fa.session_id = '" . $db->sql_escape($this->session_id) . "') WHERE f.forum_password <> ''"; $result = $db->sql_query($sql); $forum_ids = array(); while ($row = $db->sql_fetchrow($result)) { $forum_id = (int) $row['forum_id']; if ($row['user_id'] != $this->data['user_id']) { $forum_ids[$forum_id] = $forum_id; } } $db->sql_freeresult($result); return $forum_ids; } }