<?php /* Question2Answer by Gideon Greenspan and contributors http://www.question2answer.org/ File: qa-include/qa-app-users.php Version: See define()s at top of qa-include/qa-base.php Description: User management (application level) for basic user operations This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. More about this license: http://www.question2answer.org/license.php */ if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser header('Location: ../'); exit; } define('QA_USER_LEVEL_BASIC', 0); define('QA_USER_LEVEL_APPROVED', 10); define('QA_USER_LEVEL_EXPERT', 20); define('QA_USER_LEVEL_EDITOR', 50); define('QA_USER_LEVEL_MODERATOR', 80); define('QA_USER_LEVEL_ADMIN', 100); define('QA_USER_LEVEL_SUPER', 120); define('QA_USER_FLAGS_EMAIL_CONFIRMED', 1); define('QA_USER_FLAGS_USER_BLOCKED', 2); define('QA_USER_FLAGS_SHOW_AVATAR', 4); define('QA_USER_FLAGS_SHOW_GRAVATAR', 8); define('QA_USER_FLAGS_NO_MESSAGES', 16); define('QA_USER_FLAGS_NO_MAILINGS', 32); define('QA_USER_FLAGS_WELCOME_NOTICE', 64); define('QA_USER_FLAGS_MUST_CONFIRM', 128); define('QA_USER_FLAGS_NO_WALL_POSTS', 256); define('QA_USER_FLAGS_MUST_APPROVE', 512); define('QA_FIELD_FLAGS_MULTI_LINE', 1); define('QA_FIELD_FLAGS_LINK_URL', 2); define('QA_FIELD_FLAGS_ON_REGISTER', 4); @define('QA_FORM_EXPIRY_SECS', 86400); // how many seconds a form is valid for submission @define('QA_FORM_KEY_LENGTH', 32); if (QA_FINAL_EXTERNAL_USERS) { // If we're using single sign-on integration (WordPress or otherwise), load PHP file for that if (defined('QA_FINAL_WORDPRESS_INTEGRATE_PATH')) require_once QA_INCLUDE_DIR.'qa-external-users-wp.php'; else require_once QA_EXTERNAL_DIR.'qa-external-users.php'; // Access functions for user information function qa_get_logged_in_user_cache() /* Return array of information about the currently logged in user, cache to ensure only one call to external code */ { global $qa_cached_logged_in_user; if (!isset($qa_cached_logged_in_user)) { $user=qa_get_logged_in_user(); $qa_cached_logged_in_user=isset($user) ? $user : false; // to save trying again } return @$qa_cached_logged_in_user; } function qa_get_logged_in_user_field($field) /* Return $field of the currently logged in user, or null if not available */ { $user=qa_get_logged_in_user_cache(); return @$user[$field]; } function qa_get_logged_in_userid() /* Return the userid of the currently logged in user, or null if none */ { return qa_get_logged_in_user_field('userid'); } function qa_get_logged_in_points() /* Return the number of points of the currently logged in user, or null if none is logged in */ { global $qa_cached_logged_in_points; if (!isset($qa_cached_logged_in_points)) { require_once QA_INCLUDE_DIR.'qa-db-selects.php'; $qa_cached_logged_in_points=qa_db_select_with_pending(qa_db_user_points_selectspec(qa_get_logged_in_userid(), true)); } return $qa_cached_logged_in_points['points']; } function qa_get_external_avatar_html($userid, $size, $padding=false) /* Return HTML to display for the avatar of $userid, constrained to $size pixels, with optional $padding to that size */ { if (function_exists('qa_avatar_html_from_userid')) return qa_avatar_html_from_userid($userid, $size, $padding); else return null; } } else { function qa_start_session() /* Open a PHP session if one isn't opened already */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } @ini_set('session.gc_maxlifetime', 86400); // worth a try, but won't help in shared hosting environment @ini_set('session.use_trans_sid', false); // sessions need cookies to work, since we redirect after login @ini_set('session.cookie_domain', QA_COOKIE_DOMAIN); if (!isset($_SESSION)) session_start(); } function qa_session_var_suffix() /* Returns a suffix to be used for names of session variables to prevent them being shared between multiple Q2A sites on the same server */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $prefix=defined('QA_MYSQL_USERS_PREFIX') ? QA_MYSQL_USERS_PREFIX : QA_MYSQL_TABLE_PREFIX; return md5(QA_FINAL_MYSQL_HOSTNAME.'/'.QA_FINAL_MYSQL_USERNAME.'/'.QA_FINAL_MYSQL_PASSWORD.'/'.QA_FINAL_MYSQL_DATABASE.'/'.$prefix); } function qa_session_verify_code($userid) /* Returns a verification code used to ensure that a user session can't be generated by another PHP script running on the same server */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } return sha1($userid.'/'.QA_MYSQL_TABLE_PREFIX.'/'.QA_FINAL_MYSQL_DATABASE.'/'.QA_FINAL_MYSQL_PASSWORD.'/'.QA_FINAL_MYSQL_USERNAME.'/'.QA_FINAL_MYSQL_HOSTNAME); } function qa_set_session_cookie($handle, $sessioncode, $remember) /* Set cookie in browser for username $handle with $sessioncode (in database). Pass true if user checked 'Remember me' (either now or previously, as learned from cookie). */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } // if $remember is true, store in browser for a month, otherwise store only until browser is closed setcookie('qa_session', $handle.'/'.$sessioncode.'/'.($remember ? 1 : 0), $remember ? (time()+2592000) : 0, '/', QA_COOKIE_DOMAIN); } function qa_clear_session_cookie() /* Remove session cookie from browser */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } setcookie('qa_session', false, 0, '/', QA_COOKIE_DOMAIN); } function qa_set_session_user($userid, $source) /* Set the session variables to indicate that $userid is logged in from $source */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $suffix=qa_session_var_suffix(); $_SESSION['qa_session_userid_'.$suffix]=$userid; $_SESSION['qa_session_source_'.$suffix]=$source; $_SESSION['qa_session_verify_'.$suffix]=qa_session_verify_code($userid); // prevents one account on a shared server being able to create a log in a user to Q2A on another account on same server } function qa_clear_session_user() /* Clear the session variables indicating that a user is logged in */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $suffix=qa_session_var_suffix(); unset($_SESSION['qa_session_userid_'.$suffix]); unset($_SESSION['qa_session_source_'.$suffix]); unset($_SESSION['qa_session_verify_'.$suffix]); } function qa_set_logged_in_user($userid, $handle='', $remember=false, $source=null) /* Call for successful log in by $userid and $handle or successful log out with $userid=null. $remember states if 'Remember me' was checked in the login form. */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } require_once QA_INCLUDE_DIR.'qa-app-cookies.php'; qa_start_session(); if (isset($userid)) { qa_set_session_user($userid, $source); // PHP sessions time out too quickly on the server side, so we also set a cookie as backup. // Logging in from a second browser will make the previous browser's 'Remember me' no longer // work - I'm not sure if this is the right behavior - could see it either way. require_once QA_INCLUDE_DIR.'qa-db-selects.php'; $userinfo=qa_db_single_select(qa_db_user_account_selectspec($userid, true)); // if we have logged in before, and are logging in the same way as before, we don't need to change the sessioncode/source // this means it will be possible to automatically log in (via cookies) to the same account from more than one browser if (empty($userinfo['sessioncode']) || ($source!==$userinfo['sessionsource'])) { $sessioncode=qa_db_user_rand_sessioncode(); qa_db_user_set($userid, 'sessioncode', $sessioncode); qa_db_user_set($userid, 'sessionsource', $source); } else $sessioncode=$userinfo['sessioncode']; qa_db_user_logged_in($userid, qa_remote_ip_address()); qa_set_session_cookie($handle, $sessioncode, $remember); qa_report_event('u_login', $userid, $userinfo['handle'], qa_cookie_get()); } else { $olduserid=qa_get_logged_in_userid(); $oldhandle=qa_get_logged_in_handle(); qa_clear_session_cookie(); qa_clear_session_user(); qa_report_event('u_logout', $olduserid, $oldhandle, qa_cookie_get()); } } function qa_log_in_external_user($source, $identifier, $fields) /* Call to log in a user based on an external identity provider $source with external $identifier A new user is created based on $fields if it's a new combination of $source and $identifier */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } require_once QA_INCLUDE_DIR.'qa-db-users.php'; $users=qa_db_user_login_find($source, $identifier); $countusers=count($users); if ($countusers>1) qa_fatal_error('External login mapped to more than one user'); // should never happen if ($countusers) // user exists so log them in qa_set_logged_in_user($users[0]['userid'], $users[0]['handle'], false, $source); else { // create and log in user require_once QA_INCLUDE_DIR.'qa-app-users-edit.php'; qa_db_user_login_sync(true); $users=qa_db_user_login_find($source, $identifier); // check again after table is locked if (count($users)==1) { qa_db_user_login_sync(false); qa_set_logged_in_user($users[0]['userid'], $users[0]['handle'], false, $source); } else { $handle=qa_handle_make_valid(@$fields['handle']); if (strlen(@$fields['email'])) { // remove email address if it will cause a duplicate $emailusers=qa_db_user_find_by_email($fields['email']); if (count($emailusers)) { qa_redirect('login', array('e' => $fields['email'], 'ee' => '1')); unset($fields['email']); unset($fields['confirmed']); } } $userid=qa_create_new_user((string)@$fields['email'], null /* no password */, $handle, isset($fields['level']) ? $fields['level'] : QA_USER_LEVEL_BASIC, @$fields['confirmed']); qa_db_user_login_add($userid, $source, $identifier); qa_db_user_login_sync(false); $profilefields=array('name', 'location', 'website', 'about'); foreach ($profilefields as $fieldname) if (strlen(@$fields[$fieldname])) qa_db_user_profile_set($userid, $fieldname, $fields[$fieldname]); if (strlen(@$fields['avatar'])) qa_set_user_avatar($userid, $fields['avatar']); qa_set_logged_in_user($userid, $handle, false, $source); } } } function qa_get_logged_in_userid() /* Return the userid of the currently logged in user, or null if none logged in */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } global $qa_logged_in_userid_checked; $suffix=qa_session_var_suffix(); if (!$qa_logged_in_userid_checked) { // only check once qa_start_session(); // this will load logged in userid from the native PHP session, but that's not enough $sessionuserid=@$_SESSION['qa_session_userid_'.$suffix]; if (isset($sessionuserid)) // check verify code matches if (@$_SESSION['qa_session_verify_'.$suffix] != qa_session_verify_code($sessionuserid)) qa_clear_session_user(); if (!empty($_COOKIE['qa_session'])) { @list($handle, $sessioncode, $remember)=explode('/', $_COOKIE['qa_session']); if ($remember) qa_set_session_cookie($handle, $sessioncode, $remember); // extend 'remember me' cookies each time $sessioncode=trim($sessioncode); // trim to prevent passing in blank values to match uninitiated DB rows // Try to recover session from the database if PHP session has timed out if ( (!isset($_SESSION['qa_session_userid_'.$suffix])) && (!empty($handle)) && (!empty($sessioncode)) ) { require_once QA_INCLUDE_DIR.'qa-db-selects.php'; $userinfo=qa_db_single_select(qa_db_user_account_selectspec($handle, false)); // don't get any pending if (strtolower(trim($userinfo['sessioncode'])) == strtolower($sessioncode)) qa_set_session_user($userinfo['userid'], $userinfo['sessionsource']); else qa_clear_session_cookie(); // if cookie not valid, remove it to save future checks } } $qa_logged_in_userid_checked=true; } return @$_SESSION['qa_session_userid_'.$suffix]; } function qa_get_logged_in_source() /* Get the source of the currently logged in user, from call to qa_log_in_external_user() or null if logged in normally */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $userid=qa_get_logged_in_userid(); $suffix=qa_session_var_suffix(); if (isset($userid)) return @$_SESSION['qa_session_source_'.$suffix]; } function qa_get_logged_in_user_field($field) /* Return $field of the currently logged in user, cache to ensure only one call to external code */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } global $qa_cached_logged_in_user; $userid=qa_get_logged_in_userid(); if (isset($userid) && !isset($qa_cached_logged_in_user)) { require_once QA_INCLUDE_DIR.'qa-db-selects.php'; $qa_cached_logged_in_user=qa_db_get_pending_result('loggedinuser', qa_db_user_account_selectspec($userid, true)); if (!isset($qa_cached_logged_in_user)) { // the user can no longer be found (should only apply to deleted users) qa_clear_session_user(); qa_redirect(''); // implicit exit; } } return @$qa_cached_logged_in_user[$field]; } function qa_get_logged_in_points() /* Return the number of points of the currently logged in user, or null if none is logged in */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } return qa_get_logged_in_user_field('points'); } function qa_get_mysql_user_column_type() /* Return column type to use for users (if not using single sign-on integration) */ { return 'INT UNSIGNED'; } function qa_get_one_user_html($handle, $microformats=false, $favorited=false) /* Return HTML to display for user with username $handle, with microformats if $microformats is true. Set $favorited to true to show the user as favorited. */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } if (!strlen($handle)) return ''; $url = qa_path_html('user/'.$handle); $favclass = $favorited ? ' qa-user-favorited' : ''; $mfclass = $microformats ? ' url nickname' : ''; return '<a href="'.$url.'" class="qa-user-link'.$favclass.$mfclass.'">'.qa_html($handle).'</a>'; } function qa_get_user_avatar_html($flags, $email, $handle, $blobid, $width, $height, $size, $padding=false) /* Return HTML to display for the user's avatar, constrained to $size pixels, with optional $padding to that size Pass the user's fields $flags, $email, $handle, and avatar $blobid, $width and $height */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } require_once QA_INCLUDE_DIR.'qa-app-format.php'; if (qa_opt('avatar_allow_gravatar') && ($flags & QA_USER_FLAGS_SHOW_GRAVATAR)) $html=qa_get_gravatar_html($email, $size); elseif (qa_opt('avatar_allow_upload') && (($flags & QA_USER_FLAGS_SHOW_AVATAR)) && isset($blobid)) $html=qa_get_avatar_blob_html($blobid, $width, $height, $size, $padding); elseif ( (qa_opt('avatar_allow_gravatar')||qa_opt('avatar_allow_upload')) && qa_opt('avatar_default_show') && strlen(qa_opt('avatar_default_blobid')) ) $html=qa_get_avatar_blob_html(qa_opt('avatar_default_blobid'), qa_opt('avatar_default_width'), qa_opt('avatar_default_height'), $size, $padding); else $html=null; return (isset($html) && strlen($handle)) ? ('<a href="'.qa_path_html('user/'.$handle).'" class="qa-avatar-link">'.$html.'</a>') : $html; } function qa_get_user_email($userid) /* Return email address for user $userid (if not using single sign-on integration) */ { $userinfo=qa_db_select_with_pending(qa_db_user_account_selectspec($userid, true)); return $userinfo['email']; } function qa_user_report_action($userid, $action) /* Called after a database write $action performed by a user $userid */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } require_once QA_INCLUDE_DIR.'qa-db-users.php'; qa_db_user_written($userid, qa_remote_ip_address()); } function qa_user_level_string($level) /* Return textual representation of the user $level */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } if ($level>=QA_USER_LEVEL_SUPER) $string='users/level_super'; elseif ($level>=QA_USER_LEVEL_ADMIN) $string='users/level_admin'; elseif ($level>=QA_USER_LEVEL_MODERATOR) $string='users/level_moderator'; elseif ($level>=QA_USER_LEVEL_EDITOR) $string='users/level_editor'; elseif ($level>=QA_USER_LEVEL_EXPERT) $string='users/level_expert'; elseif ($level>=QA_USER_LEVEL_APPROVED) $string='users/approved_user'; else $string='users/registered_user'; return qa_lang($string); } function qa_get_login_links($rooturl, $tourl) /* Return an array of links to login, register, email confirm and logout pages (if not using single sign-on integration) */ { return array( 'login' => qa_path('login', isset($tourl) ? array('to' => $tourl) : null, $rooturl), 'register' => qa_path('register', isset($tourl) ? array('to' => $tourl) : null, $rooturl), 'confirm' => qa_path('confirm', null, $rooturl), 'logout' => qa_path('logout', null, $rooturl), ); } } // end of: if (QA_FINAL_EXTERNAL_USERS) { ... } else { ... } function qa_is_logged_in() /* Return whether someone is logged in at the moment */ { $userid=qa_get_logged_in_userid(); return isset($userid); } function qa_get_logged_in_handle() /* Return displayable handle/username of currently logged in user, or null if none */ { return qa_get_logged_in_user_field(QA_FINAL_EXTERNAL_USERS ? 'publicusername' : 'handle'); } function qa_get_logged_in_email() /* Return email of currently logged in user, or null if none */ { return qa_get_logged_in_user_field('email'); } function qa_get_logged_in_level() /* Return level of currently logged in user, or null if none */ { return qa_get_logged_in_user_field('level'); } function qa_get_logged_in_flags() /* Return flags (see QA_USER_FLAGS_*) of currently logged in user, or null if none */ { if (QA_FINAL_EXTERNAL_USERS) return qa_get_logged_in_user_field('blocked') ? QA_USER_FLAGS_USER_BLOCKED : 0; else return qa_get_logged_in_user_field('flags'); } function qa_get_logged_in_levels() /* Return an array of all the specific (e.g. per category) level privileges for the logged in user, retrieving from the database if necessary */ { require_once QA_INCLUDE_DIR.'qa-db-selects.php'; return qa_db_get_pending_result('userlevels', qa_db_user_levels_selectspec(qa_get_logged_in_userid(), true)); } function qa_userids_to_handles($userids) /* Return an array mapping each userid in $userids to that user's handle (public username), or to null if not found */ { if (QA_FINAL_EXTERNAL_USERS) $rawuseridhandles=qa_get_public_from_userids($userids); else { require_once QA_INCLUDE_DIR.'qa-db-users.php'; $rawuseridhandles=qa_db_user_get_userid_handles($userids); } $gotuseridhandles=array(); foreach ($userids as $userid) $gotuseridhandles[$userid]=@$rawuseridhandles[$userid]; return $gotuseridhandles; } function qa_userid_to_handle($userid) /* Return an string mapping the received userid to that user's handle (public username), or to null if not found */ { $handles=qa_userids_to_handles(array($userid)); return empty($handles) ? null : $handles[$userid]; } function qa_handles_to_userids($handles, $exactonly=false) /* Return an array mapping each handle in $handles the user's userid, or null if not found. If $exactonly is true then $handles must have the correct case and accents. Otherwise, handles are case- and accent-insensitive, and the keys of the returned array will match the $handles provided, not necessary those in the DB. */ { require_once QA_INCLUDE_DIR.'qa-util-string.php'; if (QA_FINAL_EXTERNAL_USERS) $rawhandleuserids=qa_get_userids_from_public($handles); else { require_once QA_INCLUDE_DIR.'qa-db-users.php'; $rawhandleuserids=qa_db_user_get_handle_userids($handles); } $gothandleuserids=array(); if ($exactonly) { // only take the exact matches foreach ($handles as $handle) $gothandleuserids[$handle]=@$rawhandleuserids[$handle]; } else { // normalize to lowercase without accents, and then find matches $normhandleuserids=array(); foreach ($rawhandleuserids as $handle => $userid) $normhandleuserids[qa_string_remove_accents(qa_strtolower($handle))]=$userid; foreach ($handles as $handle) $gothandleuserids[$handle]=@$normhandleuserids[qa_string_remove_accents(qa_strtolower($handle))]; } return $gothandleuserids; } function qa_handle_to_userid($handle) /* Return the userid corresponding to $handle (not case- or accent-sensitive) */ { if (QA_FINAL_EXTERNAL_USERS) $handleuserids=qa_get_userids_from_public(array($handle)); else { require_once QA_INCLUDE_DIR.'qa-db-users.php'; $handleuserids=qa_db_user_get_handle_userids(array($handle)); } if (count($handleuserids)==1) return reset($handleuserids); // don't use $handleuserids[$handle] since capitalization might be different return null; } function qa_user_level_for_categories($categoryids) /* Return the level of the logged in user for a post with $categoryids (expressing the full hierarchy to the final category) */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } require_once QA_INCLUDE_DIR.'qa-app-updates.php'; $level=qa_get_logged_in_level(); if (count($categoryids)) { $userlevels=qa_get_logged_in_levels(); $categorylevels=array(); // create a map foreach ($userlevels as $userlevel) if ($userlevel['entitytype']==QA_ENTITY_CATEGORY) $categorylevels[$userlevel['entityid']]=$userlevel['level']; foreach ($categoryids as $categoryid) $level=max($level, @$categorylevels[$categoryid]); } return $level; } function qa_user_level_for_post($post) /* Return the level of the logged in user for $post, as retrieved from the database */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } if (strlen(@$post['categoryids'])) return qa_user_level_for_categories(explode(',', $post['categoryids'])); return null; } function qa_user_level_maximum() /* Return the maximum possible level of the logged in user in any context (i.e. for any category) */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $level=qa_get_logged_in_level(); $userlevels=qa_get_logged_in_levels(); foreach ($userlevels as $userlevel) $level=max($level, $userlevel['level']); return $level; } function qa_user_post_permit_error($permitoption, $post, $limitaction=null, $checkblocks=true) /* Check whether the logged in user has permission to perform $permitoption on post $post (from the database) Other parameters and the return value are as for qa_user_permit_error(...) */ { return qa_user_permit_error($permitoption, $limitaction, qa_user_level_for_post($post), $checkblocks); } function qa_user_maximum_permit_error($permitoption, $limitaction=null, $checkblocks=true) /* Check whether the logged in user would have permittion to perform $permitoption in any context (i.e. for any category) Other parameters and the return value are as for qa_user_permit_error(...) */ { return qa_user_permit_error($permitoption, $limitaction, qa_user_level_maximum(), $checkblocks); } function qa_user_permit_error($permitoption=null, $limitaction=null, $userlevel=null, $checkblocks=true) /* Check whether the logged in user has permission to perform $permitoption. If $permitoption is null, this simply checks whether the user is blocked. Optionally provide an $limitaction (see top of qa-app-limits.php) to also check against user or IP rate limits. You can pass in a QA_USER_LEVEL_* constant in $userlevel to consider the user at a different level to usual (e.g. if they are performing this action in a category for which they have elevated privileges). To ignore the user's blocked status, set $checkblocks to false. Possible results, in order of priority (i.e. if more than one reason, the first will be given): 'level' => a special privilege level (e.g. expert) or minimum number of points is required 'login' => the user should login or register 'userblock' => the user has been blocked 'ipblock' => the ip address has been blocked 'confirm' => the user should confirm their email address 'approve' => the user needs to be approved by the site admins 'limit' => the user or IP address has reached a rate limit (if $limitaction specified) false => the operation can go ahead */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } require_once QA_INCLUDE_DIR.'qa-app-limits.php'; $userid=qa_get_logged_in_userid(); if (!isset($userlevel)) $userlevel=qa_get_logged_in_level(); $flags=qa_get_logged_in_flags(); if (!$checkblocks) $flags&=~QA_USER_FLAGS_USER_BLOCKED; $error=qa_permit_error($permitoption, $userid, $userlevel, $flags); if ($checkblocks && (!$error) && qa_is_ip_blocked()) $error='ipblock'; if ((!$error) && isset($userid) && ($flags & QA_USER_FLAGS_MUST_CONFIRM) && qa_opt('confirm_user_emails')) $error='confirm'; if ((!$error) && isset($userid) && ($flags & QA_USER_FLAGS_MUST_APPROVE) && qa_opt('moderate_users')) $error='approve'; if (isset($limitaction) && !$error) if (qa_user_limits_remaining($limitaction)<=0) $error='limit'; return $error; } function qa_permit_error($permitoption, $userid, $userlevel, $userflags, $userpoints=null) /* Check whether $userid (null for no user) can perform $permitoption. Result as for qa_user_permit_error(...). If appropriate, pass the user's level in $userlevel, flags in $userflags and points in $userpoints. If $userid is currently logged in, you can set $userpoints=null to retrieve them only if necessary. */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $permit=isset($permitoption) ? qa_opt($permitoption) : QA_PERMIT_ALL; if (isset($userid) && (($permit==QA_PERMIT_POINTS) || ($permit==QA_PERMIT_POINTS_CONFIRMED) || ($permit==QA_PERMIT_APPROVED_POINTS)) ) { // deal with points threshold by converting as appropriate if ( (!isset($userpoints)) && ($userid==qa_get_logged_in_userid()) ) $userpoints=qa_get_logged_in_points(); // allow late retrieval of points (to avoid unnecessary DB query when using external users) if ($userpoints>=qa_opt($permitoption.'_points')) $permit=($permit==QA_PERMIT_APPROVED_POINTS) ? QA_PERMIT_APPROVED : (($permit==QA_PERMIT_POINTS_CONFIRMED) ? QA_PERMIT_CONFIRMED : QA_PERMIT_USERS); // convert if user has enough points else $permit=QA_PERMIT_EXPERTS; // otherwise show a generic message so they're not tempted to collect points just for this } return qa_permit_value_error($permit, $userid, $userlevel, $userflags); } function qa_permit_value_error($permit, $userid, $userlevel, $userflags) /* Check whether $userid of level $userlevel with $userflags can reach the permission level in $permit (generally retrieved from an option, but not always). Result as for qa_user_permit_error(...). */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } if ($permit>=QA_PERMIT_ALL) $error=false; elseif ($permit>=QA_PERMIT_USERS) $error=isset($userid) ? false : 'login'; elseif ($permit>=QA_PERMIT_CONFIRMED) { if (!isset($userid)) $error='login'; elseif ( QA_FINAL_EXTERNAL_USERS || // not currently supported by single sign-on integration ($userlevel>=QA_PERMIT_APPROVED) || // if user approved or assigned to a higher level, no need ($userflags & QA_USER_FLAGS_EMAIL_CONFIRMED) || // actual confirmation (!qa_opt('confirm_user_emails')) // if this option off, we can't ask it of the user ) $error=false; else $error='confirm'; } elseif ($permit>=QA_PERMIT_APPROVED) { if (!isset($userid)) $error='login'; elseif ( ($userlevel>=QA_USER_LEVEL_APPROVED) || // user has been approved (!qa_opt('moderate_users')) // if this option off, we can't ask it of the user ) $error=false; else $error='approve'; } elseif ($permit>=QA_PERMIT_EXPERTS) $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_EXPERT)) ? false : 'level'; elseif ($permit>=QA_PERMIT_EDITORS) $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_EDITOR)) ? false : 'level'; elseif ($permit>=QA_PERMIT_MODERATORS) $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_MODERATOR)) ? false : 'level'; elseif ($permit>=QA_PERMIT_ADMINS) $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_ADMIN)) ? false : 'level'; else $error=(isset($userid) && ($userlevel>=QA_USER_LEVEL_SUPER)) ? false : 'level'; if (isset($userid) && ($userflags & QA_USER_FLAGS_USER_BLOCKED) && ($error!='level')) $error='userblock'; return $error; } function qa_user_captcha_reason($userlevel=null) /* Return whether a captcha is required for posts submitted by the current user. You can pass in a QA_USER_LEVEL_* constant in $userlevel to consider the user at a different level to usual (e.g. if they are performing this action in a category for which they have elevated privileges). Possible results: 'login' => captcha required because the user is not logged in 'approve' => captcha required because the user has not been approved 'confirm' => captcha required because the user has not confirmed their email address false => captcha is not required */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $reason=false; if (!isset($userlevel)) $userlevel=qa_get_logged_in_level(); if ($userlevel < QA_USER_LEVEL_APPROVED) { // approved users and above aren't shown captchas $userid=qa_get_logged_in_userid(); if (qa_opt('captcha_on_anon_post') && !isset($userid)) $reason='login'; elseif (qa_opt('moderate_users') && qa_opt('captcha_on_unapproved')) $reason='approve'; elseif (qa_opt('confirm_user_emails') && qa_opt('captcha_on_unconfirmed') && !(qa_get_logged_in_flags() & QA_USER_FLAGS_EMAIL_CONFIRMED) ) $reason='confirm'; } return $reason; } function qa_user_use_captcha($userlevel=null) /* Return whether a captcha should be presented to the logged in user for writing posts. You can pass in a QA_USER_LEVEL_* constant in $userlevel to consider the user at a different level to usual. */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } return qa_user_captcha_reason($userlevel)!=false; } function qa_user_moderation_reason($userlevel=null) /* Return whether moderation is required for posts submitted by the current user. You can pass in a QA_USER_LEVEL_* constant in $userlevel to consider the user at a different level to usual (e.g. if they are performing this action in a category for which they have elevated privileges). Possible results: 'login' => moderation required because the user is not logged in 'approve' => moderation required because the user has not been approved 'confirm' => moderation required because the user has not confirmed their email address 'points' => moderation required because the user has insufficient points false => moderation is not required */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $reason=false; if (!isset($userlevel)) $userlevel=qa_get_logged_in_level(); if ( ($userlevel < QA_USER_LEVEL_EXPERT) && // experts and above aren't moderated qa_user_permit_error('permit_moderate') // if the user can approve posts, no point in moderating theirs ) { $userid=qa_get_logged_in_userid(); if (isset($userid)) { if (qa_opt('moderate_users') && qa_opt('moderate_unapproved') && ($userlevel<QA_USER_LEVEL_APPROVED)) $reason='approve'; elseif (qa_opt('confirm_user_emails') && qa_opt('moderate_unconfirmed') && !(qa_get_logged_in_flags() & QA_USER_FLAGS_EMAIL_CONFIRMED) ) $reason='confirm'; elseif (qa_opt('moderate_by_points') && (qa_get_logged_in_points() < qa_opt('moderate_points_limit'))) $reason='points'; } elseif (qa_opt('moderate_anon_post')) $reason='login'; } return $reason; } function qa_user_userfield_label($userfield) /* Return the label to display for $userfield as retrieved from the database, using default if no name set */ { if (isset($userfield['content'])) return $userfield['content']; else { $defaultlabels=array( 'name' => 'users/full_name', 'about' => 'users/about', 'location' => 'users/location', 'website' => 'users/website', ); if (isset($defaultlabels[$userfield['title']])) return qa_lang($defaultlabels[$userfield['title']]); } return ''; } function qa_set_form_security_key() /* Set or extend the cookie in browser of non logged-in users which identifies them for the purposes of form security (anti-CSRF protection) */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } global $qa_form_key_cookie_set; if ( (!qa_is_logged_in()) && !@$qa_form_key_cookie_set) { $qa_form_key_cookie_set=true; if (strlen(@$_COOKIE['qa_key'])!=QA_FORM_KEY_LENGTH) { require_once QA_INCLUDE_DIR.'qa-util-string.php'; $_COOKIE['qa_key']=qa_random_alphanum(QA_FORM_KEY_LENGTH); } setcookie('qa_key', $_COOKIE['qa_key'], time()+2*QA_FORM_EXPIRY_SECS, '/', QA_COOKIE_DOMAIN); // extend on every page request } } function qa_calc_form_security_hash($action, $timestamp) /* Return the form security (anti-CSRF protection) hash for an $action (any string), that can be performed within QA_FORM_EXPIRY_SECS of $timestamp (in unix seconds) by the current user. */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $salt=qa_opt('form_security_salt'); if (qa_is_logged_in()) return sha1($salt.'/'.$action.'/'.$timestamp.'/'.qa_get_logged_in_userid().'/'.qa_get_logged_in_user_field('passsalt')); else return sha1($salt.'/'.$action.'/'.$timestamp.'/'.@$_COOKIE['qa_key']); // lower security for non logged in users - code+cookie can be transferred } function qa_get_form_security_code($action) /* Return the full form security (anti-CSRF protection) code for an $action (any string) performed within QA_FORM_EXPIRY_SECS of now by the current user. */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } qa_set_form_security_key(); $timestamp=qa_opt('db_time'); return (int)qa_is_logged_in().'-'.$timestamp.'-'.qa_calc_form_security_hash($action, $timestamp); } function qa_check_form_security_code($action, $value) /* Return whether $value matches the expected form security (anti-CSRF protection) code for $action (any string) and that the code has not expired (if more than QA_FORM_EXPIRY_SECS have passed). Logs causes for suspicion. */ { if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); } $reportproblems=array(); $silentproblems=array(); if (!isset($value)) $silentproblems[]='code missing'; else if (!strlen($value)) $silentproblems[]='code empty'; else { $parts=explode('-', $value); if (count($parts)==3) { $loggedin=$parts[0]; $timestamp=$parts[1]; $hash=$parts[2]; $timenow=qa_opt('db_time'); if ($timestamp>$timenow) $reportproblems[]='time '.($timestamp-$timenow).'s in future'; elseif ($timestamp<($timenow-QA_FORM_EXPIRY_SECS)) $silentproblems[]='timeout after '.($timenow-$timestamp).'s'; if (qa_is_logged_in()) { if (!$loggedin) $silentproblems[]='now logged in'; } else { if ($loggedin) $silentproblems[]='now logged out'; else { $key=@$_COOKIE['qa_key']; if (!isset($key)) $silentproblems[]='key cookie missing'; elseif (!strlen($key)) $silentproblems[]='key cookie empty'; else if (strlen($key)!=QA_FORM_KEY_LENGTH) $reportproblems[]='key cookie '.$key.' invalid'; } } if (empty($silentproblems) && empty($reportproblems)) if (strtolower(qa_calc_form_security_hash($action, $timestamp))!=strtolower($hash)) $reportproblems[]='code mismatch'; } else $reportproblems[]='code '.$value.' malformed'; } if (count($reportproblems)) @error_log( 'PHP Question2Answer form security violation for '.$action. ' by '.(qa_is_logged_in() ? ('userid '.qa_get_logged_in_userid()) : 'anonymous'). ' ('.implode(', ', array_merge($reportproblems, $silentproblems)).')'. ' on '.@$_SERVER['REQUEST_URI']. ' via '.@$_SERVER['HTTP_REFERER'] ); return (empty($silentproblems) && empty($reportproblems)); } /* Omit PHP closing tag to help avoid accidental output */