WordPress.org

Ready to get started?Download WordPress

Forums

Friends Only
[resolved] Special characters in blog's title on login page (12 posts)

  1. policieuxjp
    Member
    Posted 3 years ago #

    Hi,

    yes again special characters, sorry for that ..

    When blog's title contains special characters like "é", it is displayed on the login screen like "blog (testé)" instead of "blog (testé)". It seems like a kind of unicode problem.

    Feel free to ask me to test patches or whatever.

    Regards
    JP

    http://wordpress.org/extend/plugins/friends-only/

  2. Gabe
    Member
    Plugin Author

    Posted 3 years ago #

    Try replacing sentry.php with the following code, and see if it solves (both) your problems:

    Let me know if it work - I haven't tested it, but think it should do the trick.

    <?php 
    
    // This function determines whether the user should be displayed the login page based on whether they have already authenticated themselves with WordPress (i.e. Administrator logged in), or with the sentry
    
    function fo_runSentry() {
    
    	// Create site URLs to test later for URL hacking or provide access to special pages (e.g. login or FeedWrangler)
    
    	$base_WP_URI = str_replace('www.','',strtolower(get_bloginfo('wpurl')));
    	$clean_URI = str_replace('www.','',strtolower('http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']));
    
    	// Load the array of email addresses and clean it up 
    
    	$PERMITTED_ADDRESSES = explode(chr(13), get_option('email_list'));
    
    	array_walk($PERMITTED_ADDRESSES, 'fo_cleanAddress');
    
    	// Load and clean the address to be notified by email
    
    	$notify_address = fo_cleanAddress(get_option('notify_address'));
    
    	// If the user is logged in then don't show the sentry
    	if (is_user_logged_in()) {
    		return;
    	}
    	// If the user is requesting a FeedWrangler feed, then don't show the sentry
    	elseif (strpos($clean_URI, $base_WP_URI.'/?feed=') === 0) {
    		return;
    	}
    	// If the user is not logged in, but they are trying to log in, then let them see the login page
    	elseif (strpos($clean_URI, $base_WP_URI.'/wp-admin/') === 0 || strpos($clean_URI, $base_WP_URI.'/wp-login.php') === 0) {
    		return;
    	}
    	// If the user is trying to access XML-RPC then don't show the sentry
    	elseif (strpos($clean_URI, $base_WP_URI.'/xmlrpc.php') === 0) {
    		return;
    	}
    
    	// Process the user provided password
    
    	if (isset($_POST['access_email'])) {
    
    		$supplied_address = strtolower(trim($_POST['access_email']));
    
    		if ( strlen($supplied_address) == 0 ) { $supplied_address = 'blank'; }
    
    		if (!in_array($supplied_address, $PERMITTED_ADDRESSES)) {
    
    	  	//Send email notifying of FAILED login
    
    		if (get_option('notify_fail') && is_email($notify_address)) {
    			wp_mail( $notify_address, "[".get_bloginfo('name')."] FAIL for ".$supplied_address,
    	  		"Failed login at ".get_bloginfo('name')." by ".$supplied_address." (".date("H:i:s").substr(microtime(),1,5).")", "From: ".$notify_address );
    		}
    
    		fo_showLoginForm(get_option('prompt_error'));
    
    	  }
    	else {
    		// set cookie if password was validated
    		setcookie("verify", md5($login.'%'.$supplied_address), 0, '/');
    
    		// Clear password protector variables
    		unset($_POST['access_login']);
    		unset($_POST['access_password']);
    		unset($_POST['Submit']);
    
    		// Send email notifying of SUCCESSFUL login
    	    if (get_option('notify_success') && is_email($notify_address)) {
    	    wp_mail( $notify_address, "[".html_entity_decode(get_bloginfo('name'))."] SUCCESS for ".$supplied_address,
    	  "Successful login at ".html_entity_decode(get_bloginfo('name'))." by ".$supplied_address." (".date("H:i:s").")", "From: ".$notify_address );
    	    }
    	  }
    	}
    
    	// Check if password cookie is set
    
    	else {
    	  if (!isset($_COOKIE['verify'])) {
    	    fo_showLoginForm("");
    	  }
    	}
    }
    
    // This function displays the login form if the user is required to authenticate with the sentry
    
    function fo_showLoginForm($error_message) {
    
    	echo "
    	<html>
    	<head>
    	<title>";
    
    	echo htmlentities(bloginfo('name'));
    
    	echo "
    	</title>
    	  <META HTTP-EQUIV='CACHE-CONTROL' CONTENT='NO-CACHE'>
    	  <META HTTP-EQUIV='PRAGMA' CONTENT='NO-CACHE'>
    
    	<style type='text/css'>
    	 body {
    	   font-family : Tahoma, Verdana, Arial;
    	   padding-left: 25%;
    	   padding-top:50px;
    	   padding-bottom: 50px;
    	   padding-right: 25%;
    	 }
    	 </style>
    
    	</head>
    
    	<body>
    
    	<p style='font-size: 2em'>";
    
    	echo htmlentities(bloginfo('name'));
    
    	echo "</p>";
    
    	if ($error_message == NULL) {
    		echo htmlentities(get_option('prompt_email'));
    	}
    	else {
    		echo $error_message;
    	}
    
    	echo "
    
    	<p>
    	<form method='post'><input style='font-size: 1.2em;' type='input' name='access_email' size='35' /><input type='submit' style='background-color: #DDDDDD; border-color: #AAAAAA; color: #000000; font-family: tahoma, verdana, arial; font-size: 1.2em;' name='Submit' value='";
    
    	echo htmlentities(get_option('prompt_submit'));
    
    	echo "' /></form>
    	</p>
    	<p><a href='";
    
    	echo bloginfo('wpurl');
    
    	echo "/wp-admin/' style='color: #CCCCCC; font-size: small;'>Administrator login >></p>
    	</body>
    	</html>";
    
    	// stop at this point
    	die();
    }
    
    function fo_cleanAddress(&$value)
    	{
    	    $value = strtolower(trim($value));
    	    return $value;
    	}
    
    ?>
  3. Gabe
    Member
    Plugin Author

    Posted 3 years ago #

    fixed code above.

  4. Gabe
    Member
    Plugin Author

    Posted 3 years ago #

    I think I jumped the gun with posting that code.

    See below for code with a couple of fixes.

    <?php 
    
    // This function determines whether the user should be displayed the login page based on whether they have already authenticated themselves with WordPress (i.e. Administrator logged in), or with the sentry
    
    function fo_runSentry() {
    
    	// Create site URLs to test later for URL hacking or provide access to special pages (e.g. login or FeedWrangler)
    
    	$base_WP_URI = str_replace('www.','',strtolower(get_bloginfo('wpurl')));
    	$clean_URI = str_replace('www.','',strtolower('http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']));
    
    	// Load the array of email addresses and clean it up 
    
    	$PERMITTED_ADDRESSES = explode(chr(13), get_option('email_list'));
    
    	array_walk($PERMITTED_ADDRESSES, 'fo_cleanAddress');
    
    	// Load and clean the address to be notified by email
    
    	$notify_address = fo_cleanAddress(get_option('notify_address'));
    
    	// If the user is logged in then don't show the sentry
    	if (is_user_logged_in()) {
    		return;
    	}
    	// If the user is requesting a FeedWrangler feed, then don't show the sentry
    	elseif (strpos($clean_URI, $base_WP_URI.'/?feed=') === 0) {
    		return;
    	}
    	// If the user is not logged in, but they are trying to log in, then let them see the login page
    	elseif (strpos($clean_URI, $base_WP_URI.'/wp-admin/') === 0 || strpos($clean_URI, $base_WP_URI.'/wp-login.php') === 0) {
    		return;
    	}
    	// If the user is trying to access XML-RPC then don't show the sentry
    	elseif (strpos($clean_URI, $base_WP_URI.'/xmlrpc.php') === 0) {
    		return;
    	}
    
    	// Process the user provided password
    
    	if (isset($_POST['access_email'])) {
    
    		$supplied_address = strtolower(trim($_POST['access_email']));
    
    		if ( strlen($supplied_address) == 0 ) { $supplied_address = 'blank'; }
    
    		if (!in_array($supplied_address, $PERMITTED_ADDRESSES)) {
    
    	  	//Send email notifying of FAILED login
    
    		if (get_option('notify_fail') && is_email($notify_address)) {
    			wp_mail( $notify_address, "[".html_entity_decode(get_bloginfo('name'))."] FAIL for ".$supplied_address,
    	  		"Failed login at ".html_entity_decode(get_bloginfo('name'))." by ".$supplied_address." (".date("H:i:s").substr(microtime(),1,5).")", "From: ".$notify_address );
    		}
    
    		fo_showLoginForm(get_option('prompt_error'));
    
    	  }
    	else {
    		// set cookie if password was validated
    		setcookie("verify", md5($login.'%'.$supplied_address), 0, '/');
    
    		// Clear password protector variables
    		unset($_POST['access_login']);
    		unset($_POST['access_password']);
    		unset($_POST['Submit']);
    
    		// Send email notifying of SUCCESSFUL login
    	    if (get_option('notify_success') && is_email($notify_address)) {
    	    wp_mail( $notify_address, "[".html_entity_decode(get_bloginfo('name'))."] SUCCESS for ".$supplied_address,
    	  "Successful login at ".html_entity_decode(get_bloginfo('name'))." by ".$supplied_address." (".date("H:i:s").")", "From: ".$notify_address );
    	    }
    	  }
    	}
    
    	// Check if password cookie is set
    
    	else {
    	  if (!isset($_COOKIE['verify'])) {
    	    fo_showLoginForm("");
    	  }
    	}
    }
    
    // This function displays the login form if the user is required to authenticate with the sentry
    
    function fo_showLoginForm($error_message) {
    
    	echo "
    	<html>
    	<head>
    	<title>";
    
    	echo htmlentities(bloginfo('name'));
    
    	echo "
    	</title>
    	  <META HTTP-EQUIV='CACHE-CONTROL' CONTENT='NO-CACHE'>
    	  <META HTTP-EQUIV='PRAGMA' CONTENT='NO-CACHE'>
    
    	<style type='text/css'>
    	 body {
    	   font-family : Tahoma, Verdana, Arial;
    	   padding-left: 25%;
    	   padding-top:50px;
    	   padding-bottom: 50px;
    	   padding-right: 25%;
    	 }
    	 </style>
    
    	</head>
    
    	<body>
    
    	<p style='font-size: 2em'>";
    
    	echo htmlentities(bloginfo('name'));
    
    	echo "</p>";
    
    	if ($error_message == NULL) {
    		echo htmlentities(get_option('prompt_email'));
    	}
    	else {
    		echo htmlentities($error_message);
    	}
    
    	echo "
    	<br />
    	<p>
    	<form method='post'><input style='font-size: 1.2em;' type='input' name='access_email' size='35' /><input type='submit' style='background-color: #DDDDDD; border-color: #AAAAAA; color: #000000; font-family: tahoma, verdana, arial; font-size: 1.2em;' name='Submit' value='";
    
    	echo htmlentities(get_option('prompt_submit'));
    
    	echo "' /></form>
    	</p>
    	<br /><p><a href='";
    
    	echo bloginfo('wpurl');
    
    	echo "/wp-admin/' style='color: #CCCCCC; font-size: small;'>Administrator login >></a></p>
    	</body>
    	</html>";
    
    	// stop at this point
    	die();
    }
    
    function fo_cleanAddress(&$value)
    	{
    	    $value = strtolower(trim($value));
    	    return $value;
    	}
    
    ?>
  5. policieuxjp
    Member
    Posted 3 years ago #

    Thanks for your help, I've tried your code but it did not work on my plateform. I went deeper in the code, thinking about how it works in other pages in wordpress.

    For the unicode problem, the page needs another meta tag at the beginning to specify charset.

    For the notification mails, I found a wp specific function that solves the problem : wp_specialchars_decode

    Below is the resulting code for sentry.php, let me know if it works fine for you.

    <?php 
    
    // This function determines whether the user should be displayed the login page based on whether they have already authenticated themselves with WordPress (i.e. Administrator logged in), or with the sentry
    
    function fo_runSentry() {
    
    	// Create site URLs to test later for URL hacking or provide access to special pages (e.g. login or FeedWrangler)
    
    	$base_WP_URI = str_replace('www.','',strtolower(get_bloginfo('wpurl')));
    	$clean_URI = str_replace('www.','',strtolower('http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']));
    
    	// Load the array of email addresses and clean it up 
    
    	$PERMITTED_ADDRESSES = explode(chr(13), get_option('email_list'));
    
    	array_walk($PERMITTED_ADDRESSES, 'fo_cleanAddress');
    
    	// Load and clean the address to be notified by email
    
    	$notify_address = fo_cleanAddress(get_option('notify_address'));
    
    	// If the user is logged in then don't show the sentry
    	if (is_user_logged_in()) {
    		return;
    	}
    	// If the user is requesting a FeedWrangler feed, then don't show the sentry
    	elseif (strpos($clean_URI, $base_WP_URI.'/?feed=') === 0) {
    		return;
    	}
    	// If the user is not logged in, but they are trying to log in, then let them see the login page
    	elseif (strpos($clean_URI, $base_WP_URI.'/wp-admin/') === 0 || strpos($clean_URI, $base_WP_URI.'/wp-login.php') === 0) {
    		return;
    	}
    	// If the user is trying to access XML-RPC then don't show the sentry
    	elseif (strpos($clean_URI, $base_WP_URI.'/xmlrpc.php') === 0) {
    		return;
    	}
    
    	// Process the user provided password
    
    	if (isset($_POST['access_email'])) {
    
    		$supplied_address = strtolower(trim($_POST['access_email']));
    
    		if ( strlen($supplied_address) == 0 ) { $supplied_address = 'blank'; }
    
    		if (!in_array($supplied_address, $PERMITTED_ADDRESSES)) {
    
    	  	//Send email notifying of FAILED login
    
    		if (get_option('notify_fail') && is_email($notify_address)) {
    			wp_mail( $notify_address, "[".wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)."] FAIL for ".$supplied_address,
    	  		"Failed login at ".wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)." by ".$supplied_address." (".date("H:i:s").substr(microtime(),1,5).")", "From: ".$notify_address );
    		}
    		fo_showLoginForm(get_option('prompt_error'));
    
    	  }
    	else {
    		// set cookie if password was validated
    		setcookie("verify", md5($login.'%'.$supplied_address), 0, '/');
    
    		// Clear password protector variables
    		unset($_POST['access_login']);
    		unset($_POST['access_password']);
    		unset($_POST['Submit']);
    
    		// Send email notifying of SUCCESSFUL login
    	    if (get_option('notify_success') && is_email($notify_address)) {
    	    wp_mail( $notify_address, "[".wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)."] SUCCESS for ".$supplied_address,
    	  "Successful login at ".wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)." by ".$supplied_address." (".date("H:i:s").substr(microtime(),1,5).")", "From: ".$notify_address );
    	    }
    	  }
    	}
    
    	// Check if password cookie is set
    
    	else {
    	  if (!isset($_COOKIE['verify'])) {
    	    fo_showLoginForm("");
    	  }
    	}
    }
    
    // This function displays the login form if the user is required to authenticate with the sentry
    
    function fo_showLoginForm($error_message) {
    
    	echo "
    	<html>
    	<head>
    	<title>";
    
    	echo bloginfo('name');
    
    	echo "
    	</title>
    	  <META HTTP-EQUIV='CACHE-CONTROL' CONTENT='NO-CACHE'>
    	  <META HTTP-EQUIV='PRAGMA' CONTENT='NO-CACHE'>
    	  <META HTTP-EQUIV='CONTENT-TYPE' CONTENT='";
    
    	echo bloginfo('html_type');
    	echo " CHARSET=";
    	echo bloginfo('charset');
    	echo "' >
    
    	<style type='text/css'>
    	 body {
    	   font-family : Tahoma, Verdana, Arial;
    	   padding-left: 25%;
    	   padding-top:50px;
    	   padding-bottom: 50px;
    	   padding-right: 25%;
    	 }
    	 </style>
    
    	</head>
    
    	<body>
    
    	<p style='font-size: 2em'>";
    
    	echo bloginfo('name');
    
    	echo "</p>";
    
    	if ($error_message == NULL) {
    		echo get_option('prompt_email');
    	}
    	else {
    		echo $error_message;
    	}
    
    	echo "
    	<br />
    	<p>
    	<form method='post'><input style='font-size: 1.2em;' type='input' name='access_email' size='35' /><input type='submit' style='background-color: #DDDDDD; border-color: #AAAAAA; color: #000000; font-family: tahoma, verdana, arial; font-size: 1.2em;' name='Submit' value='";
    
    	echo get_option('prompt_submit');
    
    	echo "' /></form>
    	</p>
    	<br /><p><a href='";
    
    	echo bloginfo('wpurl');
    
    	echo "/wp-admin/' style='color: #CCCCCC; font-size: small;'>Administrator login >></a></p>
    	</body>
    	</html>";
    
    	// stop at this point
    	die();
    }
    
    function fo_cleanAddress(&$value)
    	{
    	    $value = strtolower(trim($value));
    	    return $value;
    	}
    
    ?>
  6. Gabe
    Member
    Plugin Author

    Posted 3 years ago #

    Okay, can you check this code out?

    I think I got all your changes, and I also made some other tweaks to the email messages.

    Thanks!

    <?php 
    
    // This function determines whether the user should be displayed the login page based on whether they have already authenticated themselves with WordPress (i.e. Administrator logged in), or with the sentry
    
    function fo_runSentry() {
    
    	// Create site URLs to test later for URL hacking or provide access to special pages (e.g. login or FeedWrangler)
    
    	$base_WP_URI = str_replace('www.','',strtolower(get_bloginfo('wpurl')));
    	$clean_URI = str_replace('www.','',strtolower('http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']));
    
    	// Load the array of email addresses and clean it up 
    
    	$PERMITTED_ADDRESSES = explode(chr(13), get_option('email_list'));
    
    	array_walk($PERMITTED_ADDRESSES, 'fo_cleanAddress');
    
    	// Load and clean the address to be notified by email
    
    	$notify_address = fo_cleanAddress(get_option('notify_address'));
    
    	// If the user is logged in then don't show the sentry
    	if (is_user_logged_in()) {
    		return;
    	}
    	// If the user is requesting a FeedWrangler feed, then don't show the sentry
    	elseif (strpos($clean_URI, $base_WP_URI.'/?feed=') === 0) {
    		return;
    	}
    	// If the user is not logged in, but they are trying to log in, then let them see the login page
    	elseif (strpos($clean_URI, $base_WP_URI.'/wp-admin/') === 0 || strpos($clean_URI, $base_WP_URI.'/wp-login.php') === 0) {
    		return;
    	}
    	// If the user is trying to access XML-RPC then don't show the sentry
    	elseif (strpos($clean_URI, $base_WP_URI.'/xmlrpc.php') === 0) {
    		return;
    	}
    
    	// Process the user provided password
    
    	if (isset($_POST['access_email'])) {
    
    		$supplied_address = strtolower(trim($_POST['access_email']));
    
    		if ( strlen($supplied_address) == 0 ) { $supplied_address = 'blank'; }
    
    		if (!in_array($supplied_address, $PERMITTED_ADDRESSES)) {
    
    	  	//Send email notifying of FAILED login
    
    		if (get_option('notify_fail') && is_email($notify_address)) {
    			wp_mail( $notify_address, "[".wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)."] FAIL for ".$supplied_address,
    	  		"Failed login at ".wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)." by ".$supplied_address."\n\nTime: ".date("H:i:s \(\G\M\TP\)")."\nIP: ".$_SERVER['REMOTE_ADDR']." (http://www.ip2location.com/".$_SERVER['REMOTE_ADDR'].")", "From: ".$notify_address );
    		}
    
    		fo_showLoginForm(get_option('prompt_error'));
    
    	  }
    	else {
    		// set cookie if password was validated
    		setcookie("verify", md5($login.'%'.$supplied_address), 0, '/');
    
    		// Clear password protector variables
    		unset($_POST['access_login']);
    		unset($_POST['access_password']);
    		unset($_POST['Submit']);
    
    		// Send email notifying of SUCCESSFUL login
    	    if (get_option('notify_success') && is_email($notify_address)) {
    	    wp_mail( $notify_address, "[".wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)."] SUCCESS for ".$supplied_address,
    	  "Successful login at ".wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES)." by ".$supplied_address."\n\nTime: ".date("H:i:s \(\G\M\TP\)")."\nIP: ".$_SERVER['REMOTE_ADDR']." (http://www.ip2location.com/".$_SERVER['REMOTE_ADDR'].")", "From: ".$notify_address );
    	    }
    	  }
    	}
    
    	// Check if password cookie is set
    
    	else {
    	  if (!isset($_COOKIE['verify'])) {
    	    fo_showLoginForm("");
    	  }
    	}
    }
    
    // This function displays the login form if the user is required to authenticate with the sentry
    
    function fo_showLoginForm($error_message) {
    
    	echo "
    	<html>
    	<head>
    	<title>";
    
    	echo htmlentities(bloginfo('name'));
    
    	echo "
    	</title>
    	  <META HTTP-EQUIV='CACHE-CONTROL' CONTENT='NO-CACHE'>
    	  <META HTTP-EQUIV='PRAGMA' CONTENT='NO-CACHE'>
    	  <META HTTP-EQUIV='CONTENT-TYPE' CONTENT='";
    
    	echo bloginfo('html_type');
    	echo " CHARSET=";
    	echo bloginfo('charset');
    	echo "'>
    
    	<style type='text/css'>
    	 body {
    	   font-family : Tahoma, Verdana, Arial;
    	   padding-left: 25%;
    	   padding-top:50px;
    	   padding-bottom: 50px;
    	   padding-right: 25%;
    	 }
    	 </style>
    
    	</head>
    
    	<body>
    
    	<p style='font-size: 2em'>";
    
    	echo bloginfo('name');
    
    	echo "</p>";
    
    	if ($error_message == NULL) {
    		echo get_option('prompt_email');
    	}
    	else {
    		echo $error_message;
    	}
    
    	echo "
    	<br />
    	<p>
    	<form method='post'><input style='font-size: 1.2em;' type='input' name='access_email' size='35' /><input type='submit' style='background-color: #DDDDDD; border-color: #AAAAAA; color: #000000; font-family: tahoma, verdana, arial; font-size: 1.2em;' name='Submit' value='";
    
    	echo get_option('prompt_submit');
    
    	echo "' /></form>
    	</p>
    	<br /><p><a href='";
    
    	echo bloginfo('wpurl');
    
    	echo "/wp-admin/' style='color: #CCCCCC; font-size: small;'>Administrator login >></a></p>
    	</body>
    	</html>";
    
    	// stop at this point
    	die();
    }
    
    function fo_cleanAddress(&$value)
    	{
    	    $value = strtolower(trim($value));
    	    return $value;
    	}
    
    ?>
  7. Gabe
    Member
    Plugin Author

    Posted 3 years ago #

    JP - have you had a chance to test the code I posted above yet?

    -g

  8. policieuxjp
    Member
    Posted 3 years ago #

    Hi,

    I had just become a Daddy a few days ago, I had a very busy time, sorry .. I'll try to test the code this week !

    JP

  9. Gabe
    Member
    Plugin Author

    Posted 3 years ago #

    :) Congratulations! I'm sure you've got more important things to deal with at the moment.

  10. policieuxjp
    Member
    Posted 2 years ago #

    Hi Gabe,

    sorry for this long time silence, I forgot you asked me for a feedback on your code. I saw you've released since then. Do you still need a feedback ?

    JP

  11. Gabe
    Member
    Plugin Author

    Posted 2 years ago #

    Hi JP,

    Are you experiencing any problems with the latest version of the plugin? I incorporated the changes into the last few releases.

    If not, then we're all good!

    Gabe

  12. policieuxjp
    Member
    Posted 2 years ago #

    Not at all but I did not try the multiple recipients feature yet.

    I let you know if I find something.

    Thanks again for the plugin.

    JP

Topic Closed

This topic has been closed to new replies.

About this Plugin

About this Topic