Modifying the BuddyPress AdminBar

On UCalgaryBlogs, I’d modified the adminbar to include a link to the current site’s dashboard if a person was logged in, making it easy to get to the members-only side of WordPress without having to go through My Blogs and finding the right blog, then mousing over the pop-out “Dashboard” link. Most people never found that, and it’s not very intuitive.

So, I hacked in a hard-coded link to Dashboard in bp-core-adminbar.php. This worked, but meant I had to remember to re-hack the file after running a BuddyPress update. I forgot to do that right after I ran the last upgrade, and got emails from users asking WTF?

I decided to figure out the best way to add in the Dashboard link without hacking the actual plugin files. Turns out, it’s drop-dead simple. Yay, WordPress.

In your /wp-content/plugins/ directory, create a file called bp-custom.php (if it’s not there already), and drop this code into it:

<?php
  // custom functions for tweaking BuddyPress
  function custom_adminbar_dashboard_button() {
 	// adds a "Dashboard" link to the BuddyPress admin bar if a user is logged in.
 	if (is_user_logged_in()) {
 		echo '<li><a href="/wp-admin/">Dashboard</a></li>';
 	}
   }
  add_action('bp_adminbar_menus', 'custom_adminbar_dashboard_button', 1);
 ?>

When in place, your BuddyPress adminbar will look something like this:

BuddyPress-adminbar-modified

Yes, I know I should do something to properly detect user levels and privileges, rather than just providing the Dashboard link all willie-nillie to anyone that’s logged in, but the link itself just provides access to whatever Dashboard features the user is allowed to see, so there’s no security risk. Better to just say that a user can see the Dashboard for any site they’re logged into, and let WordPress deal with restricting access properly.

I should also deal with the possibility of WPMU being configured as a subdirectory vs. subdomain (the /wp-admin/ link will bork if you’re using subdirectories – better to use the real code to sniff out the base url of the current site…)

Fixing WPMU 2.8.4 and the ignored Banned Email Domains option

wpmufunctions_iconI’ve been having a heck of a time battling sploggers at UCalgaryBlogs.ca – roaches that create accounts and blogs so they can foist their spam links to game Google (thanks for providing spammers with such a powerful incentive, Google).

There’s an option in WordPress Multiuser to ban email domains – provide the domains, one per line, into a text box, and it will reject any roaches trying to create accounts from those domains.

The biggest offenders have been myspace.info and myspacee.info – and although they’ve been in my Banned Email Domains list for months, they just keep getting through. I figured there was some exploit they were using, but couldn’t find a thing.

So, today, I took a look through the code of WPMU 2.8.4, to see if I could find what was going on. Turns out, it’s a really simple fix. There’s a function in wp-includes/wpmu-functions.php, called is_email_address_unsafe() – it’s supposed to check the contents of the Banned Email Domains option field, and reject addresses from the flagged domains.

Except it wasn’t. Rejecting, I mean. It was letting everyone through, because of a simple bug in the code. It was written to treat the value of the option as an array and to directly walk through each item of the array. But, the option is stored as a string, so it needs to be converted to an array first. Easy peasy. Here’s my updated is_email_address_unsafe() function, which goes around line 880 of wpmu-functions.php:

function is_email_address_unsafe( $user_email ) {
	$banned_names_text = get_site_option( "banned_email_domains" ); // grab the string first
	$banned_names = explode("\n", $banned_names_text); // convert the raw text string to an array with an item per line
	if ( is_array( $banned_names ) && empty( $banned_names ) == false ) {
		$email_domain = strtolower( substr( $user_email, 1 + strpos( $user_email, '@' ) ) );
		foreach( (array) $banned_names as $banned_domain ) {
			if( $banned_domain == '' )
				continue;
			if (
				strstr( $email_domain, $banned_domain ) ||
				(
					strstr( $banned_domain, '/' ) &&
					preg_match( $banned_domain, $email_domain )
				)
			)
			return true;
		}
	}
	return false;
}

The fix is in the first 2 lines of the function – getting the value of the string, and then exploding that into the array which is then used by the rest of the function. I’ve tested the updated function out on UCalgaryBlogs.ca and it seems to work just fine. Hopefully the fix will get pulled into the next update of WPMU so everyone with Banned Email Domains can breathe a bit more easily.

BuddyPress and MultiDB

multidb_buddypress_configI’ve been trying to get BuddyPress working on my WPMU installation that uses MultiDB for database partitioning. It’s been cranky, but I just realized I’m a complete idiot because I was overlooking the obvious (and drop dead simple) fix.

BuddyPress was acting up because it was creating tables in each blog’s database tableset. But MultiDB makes it easy to declare tables as belonging to a shared global database, so they don’t get recreated for each blog and are common across the entire service.

Thanks to a reminder by Andrew on the premium.wpmudev.org forum!

I edited my db-config.php file to declare the BuddyPress tables as being global, and copied the tables from the database where they had been collecting, into the global database.

// BuddyPress
add_global_table('bp_activity_sitewide');
add_global_table('bp_activity_user_activity');
add_global_table('bp_activity_user_activity_cached');
add_global_table('bp_friends');
add_global_table('bp_groups');
add_global_table('bp_groups_groupmeta');
add_global_table('bp_groups_members');
add_global_table('bp_groups_wire');
add_global_table('bp_messages_messages');
add_global_table('bp_messages_notices');
add_global_table('bp_messages_recipients');
add_global_table('bp_messages_threads');
add_global_table('bp_notifications');
add_global_table('bp_user_blogs');
add_global_table('bp_user_blogs_blogmeta');
add_global_table('bp_user_blogs_comments');
add_global_table('bp_user_blogs_posts');
add_global_table('bp_xprofile_data');
add_global_table('bp_xprofile_fields');
add_global_table('bp_xprofile_groups');
add_global_table('bp_xprofile_wire');

It seems to be working fine. I’ll do some more testing, but it’s looking promising. If it’s really working, I’ll be spending some time to BuddyPress-enable the main theme for the WPMU service, and roll it out properly.

Stopping Spamblog Registration in WordPress MultiUser

Comment Spammers Burn In Hell...I’ve been running a copy of WordPress MultiUser for over a year now. Comment spam hasn’t been much of a problem, thanks to Akismet, but if I leave site registration open (so students and faculty can create new accounts and blogs), the evil spammers find it and start sending their bots en masse.

I tried a few plugins, with varying levels of success. There’s an interesting one that attempts to limit registrations to a specific country, but it falsely reported some valid users as not being in Canada. Captchas work, but also block some valid users (and the signup captcha plugin I’d been using is possibly abandoned).

So, I did some quick googling, and came across the page on the WordPress Codex about using .htaccess files to stop comment spam. I made some modifications to the technique, and am now running it on UCalgaryBlogs.ca with apparent success. The apache logs show the bot attacks are still in full force, but not a single one has gotten through in order to register. And valid users have been able to get through. That’s pretty promising.

Continue reading “Stopping Spamblog Registration in WordPress MultiUser”

Cleaning up after Microsoft

mswordscreenshotI spend a depressing amount of time cleaning up after Microsoft. Specifically, cleaning up the “helpful” HTML code generated by MS Word and/or Internet Exploder on Windows when people copy content from MS Word and paste it into a WYSIWYG editor in Internet Explorer. Helpful, in that it tries (and fails so spectacularly that it boggles my mind how such a “feature” was designed) and more often than not completely borks whatever website is the unsuspecting recipient of the control-V-of-death.

I’m not going to tell people not to use MS Word. It’s what people use. Trying to get them to switch to anything else would be tilting at windmills. People use Word.

I’m not going to tell people not to use Internet Explorer. I don’t use it. Nobody I work with uses it. But people do – most often, it’s people who don’t really know what a “browser” is, or that there are options, or that IE is a dangerous beast. They use IE. Fine.

But… I just found a plugin for WordPress that should at least mitigate the damage of the Word/IE duopoly.

Here’s an example. I just worked up a simple document in Word. It’s pretty fantastic. I’m proud of it. Teacher will give me an A+, for sure. It looks like this:

msword_content

It’s a work of art. Now, I copy the contents of that fantastic piece of literature, and hit control-C to copy it. I switch over to Internet Explorer, and paste it into the Visual editor on a WordPress site. And it looks kinda like hell. The source code of the pasted content looks like this:

borkedmsonormalmarkup

WTF? MsoNormal? margins? font-size and font-family? For the love of Xenu, why do you bork my content like this? Now, most people just see the result and say “Man, does WordPress suck. I’m not going to use THAT again.” – they don’t realize that it’s Word/IE that’s borking their content, and that it would be equally borked on any web-based content management system that offers a visual wysiwyg editor.

So, after activating the plugin, pasting the same content from my most awesome Word Document into the Visual editor of a WordPress site generates code like this:

cleanedupmarkup

It’s not perfect, but it’s cleaner. Some of the formatting won’t be exactly what was in the MS Word document, but that’s probably for the better. Apparently, if I used proper styles to define Headings in my document, it would convert them to h1/h2/etc… in the pasted markup. Ahhh… much better.

If you’re using WordPress with people that are using MS Word and/or Internet Explorer, get the plugin. You’ll be doing them a favour, and saving yourself some grief.

WPMU Post and Comment Growth

The group of WPMU rockstars at UBC’s OLT just whipped up a fantastic new plugin for administrators of a WPMU site to get a feel for the growth of the community. It generates a graph to display growth in numbers of blog posts and comments over time, and uses the Google Data Visualization API to let you interactively define data ranges to be graphed.

Here’s the growth of UCalgaryBlogs.ca graphed for the last 2 semesters:

ucalgaryblogs-posts-comments

Another fantastic job by the OLT blogging platform crew. Now, to just add users and pages, and it’ll be perfect… 😉

WordPress, draft/private pages, and the parent hierarchy structure

pageshierarchyI’m working with a class of 250+ geology undergrads, split up into 53 groups. They’re using a WordPress site to publish online presentations as the product of a semester-long group project. I’m using the great WP-Sentry plugin to let them collaboratively author the pages without worrying about other students in the class being able to edit their work (I know – but it makes them more comfortable so it’s a good thing to add).

The premise is this – I created a Page called, creatively enough, “Winter 2009” – and each of the groups is to create a page (or set of pages) and add them to the site – and selecting “Winter 2009” as the parent page for the main page of their presentation. They are free to create as many other pages as they like, and can set those to use their first page as the parent, thereby generating a table of contents.

Works great. Except that the WP-Sentry plugin hijacks the “Private” state of pages, and the tree of Pages available in the Parent selector is based on “Published” pages.

Conflict. Confusion. Frustration.

The students could either collaborate on the pages, or organize them in the tree structure.

Of course they could create the pages and add them to the tree structure and THEN enable the WP-Sentry-managed group editing controls, but YOU try explaining that process to 250 undergrads, all stressed out about building web pages as part of a geology course.

So… I dug into the code to see what was yanking “Private” pages from the Parent list. Turns out, it’s in wp-includes/post.php, waaaay down on line 2618 (as of WPMU 2.7). All I did was remove the " AND post_status = 'publish'" bit, and it now appears to be listing all pages.

I’m quite sure I borked something else, but for now I’m leaving the Parent list wide open until the students are done publishing their presentations.

Update: Unintended consequence #242: Looks like with the tweak, Private pages show up where they’re not expected. I’m disabling the tweak for now until I can find a better way (if that’s even possible).

wpmu activity reports using the blog_activity plugin

Jim Groom linked to a post by Patrick Murray-John with an interesting summary of the activity on UMWBlogs.org – and I was curious about what activity patterns are on UCalgaryBlogs.ca – so I fired up Sequel Pro and dug around in the raw data stored by the blog_activity plugin in the wp_post_activity and wp_comment_activity tables. The tables include aggregate and anonymous activity data for the last month.

There is a relatively new Reports plugin that could do much of this in an automated way, but it only supports generating activity reports for individual users or blogs, not aggregate reports.

Following is the MySQL code I ran to crunch the tables into usable data, which I then (cringingly) copied and pasted into (wincingly) MS Excel to generate tables and visuals.

Posts per Hour of Day

To get the number of posts published by hour of day, I ran this:

select distinct from_unixtime(stamp, "%H") as hour, count(*) as numberOfPosts from wp_post_activity group by hour order by hour;

postsperhourofday

Posts per Day of Week

select distinct from_unixtime(stamp, "%a") as day, count(*) as numberOfPosts from wp_post_activity group by day;

postsperdayofweek

Comments per Hour of Day

select distinct from_unixtime(stamp, "%H") as hour, count(*) as numberOfComments from wp_comment_activity group by hour order by hour;

commentsperhourofday

Comments per Day of Week

select distinct from_unixtime(stamp, "%a") as day, count(*) as numberOfComments from wp_comment_activity group by day;

commentsperdayofweek

Combining some of the data

Now that I’ve got the data out, it’s easy to combine sets to see what’s going on. Comments and Posts per Hour of Day:

combined_posts_comments_per_hour

and combined posts and comments per day of week:

combined_posts_comments_per_day

What’s it mean?

I don’t know what it means. Mostly, I just like shiny graphs with lines that loosely correspond to something. Am I going to read anything into it? Nope. But if nothing else, it’s interesting to see that activity isn’t tightly synchronized with in-class time

Now, it’s clear that we’re nowhere NEAR the activity level of UMWBlogs, nor do we have the sustained activity (we don’t have The Reverend, after all), but I was surprised and impressed that the aggregate activity was much higher in “off” hours/days than I’d have guessed. Actual activity, outside of classroom hours. Who’d have guessed?

notes on converting ucalgaryblogs.ca to use multi-db

out with the oldI followed Jim’s instructions to get UCalgaryBlogs.ca converted from using a single database (as is the default) to using multiple databases (17 separate databases now) via the premium.wpmudev.org Multi-DB code to prevent growing pains. The single database config is good for getting up and running, but with 300 blogs in the system, table explosion was causing grief on the shared MySQL database server – there were almost 3000 tables, which was making the automated backup script complain a bit.

While reading the documentation, I was rather confused by the term “global” – which appeared to be used in slightly different ways. Eventually, I plugged through, and got it working. The key is to test it all on a local copy of the database before running the migration script on the production server. Thankfully, the script doesn’t delete anything, so I was confident that if anything borked I could just back out the multi-db files and revert to single database config without losing anything.

“Global Tables” are tables that will be stored in a shared, common database rather than in each blog’s database in one of the 16 databases used by the multi-db code. These are things that are accessed by all blogs on the WPMU install, and include administrative stuff.

In the db-config-sample-16.php file that ships with multi-db, it also mentions “global-db”, “globaluser”, and “globalpassword” – those are just the database server address, username, and password to use when connecting to the “Global” database containing the “global tables”. They used “global-” in these parameters because it’s possible to configure each of the 17 databases to use different database servers, different usernames, and different passwords. For simplicity, I used the same database server and account for all 17 databases.

My db-config.php file was edited as follows:

<?php
//	Plugin Name: Multi-DB
//	Plugin URI: http://premium.wpmudev.org/project/Multiple-Databases
//	Author: Andrew Billits (Incsub)
//  Version: 2.7.0
//------------------------------------------------------------------------//
//---DB Scaling-----------------------------------------------------------//
//------------------------------------------------------------------------//
//	16,256,4096
define ('DB_SCALING', '16'); // use 16 databases for the blogs
//------------------------------------------------------------------------//
//---DC IPs---------------------------------------------------------------//
//------------------------------------------------------------------------//
//	Usage: add_dc_ip(IP, DC)
//	EX: add_dc_ip('123.123.123.', 'dc1');
add_dc_ip('127.0.0.1', 'dc1'); // DN: change this to the IP address of your WEB SERVER
//------------------------------------------------------------------------//
//---Global Tables--------------------------------------------------------//
//------------------------------------------------------------------------//
//	Do not include default global tables
//	Leave off base prefix (eg: wp_)
//
//	Usage: add_global_table(TABLE_NAME)
//	EX: add_global_table('something');
// DN: These are tables that will be stored in the global database configured below (wpmu_global)
//     rather than in the 16 blog databases.
add_global_table('mass_mailer');
add_global_table('registration_log');
add_global_table('reports_comment_activity');
add_global_table('reports_post_activity');
add_global_table('reports_user_activity');
add_global_table('signups');
add_global_table('support_faq');
add_global_table('support_faq_cats');
add_global_table('support_tickets');
add_global_table('support_tickets_cats');
add_global_table('support_tickets_messages');
add_global_table('domain_mapping');
add_global_table('comment_activity');
add_global_table('blog_activity');
add_global_table('user_activity');
add_global_table('post_activity');

//------------------------------------------------------------------------//
//---DB Servers-----------------------------------------------------------//
//------------------------------------------------------------------------//
//	Database servers grouped by dataset.
//	R can be 0 (no reads) or a positive integer indicating the order
//	in which to attempt communication (all locals, then all remotes)
//
//	Usage: add_db_server(DS, DC, READ, WRITE, HOST, LAN_HOST, NAME, USER, PASS)
//	EX: add_db_server('global', 'dc1', 1, 1,'global.mysql.example.com:3509','global.mysql.example.lan:3509', 'global-db', 'globaluser',  'globalpassword');
// DN: NOTE: change 'dbserver.com' to the address of the mysql server,
//   'username' to your mysql username,
//   'password' to the appropriate password.

add_db_server('global', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_global', 'username', 'password');
add_db_server('0', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_0', 'username', 'password');
add_db_server('1', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_1', 'username', 'password');
add_db_server('2', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_2', 'username', 'password');
add_db_server('3', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_3', 'username', 'password');
add_db_server('4', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_4', 'username', 'password');
add_db_server('5', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_5', 'username', 'password');
add_db_server('6', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_6', 'username', 'password');
add_db_server('7', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_7', 'username', 'password');
add_db_server('8', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_8', 'username', 'password');
add_db_server('9', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_9', 'username', 'password');
add_db_server('a', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_a', 'username', 'password');
add_db_server('b', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_b', 'username', 'password');
add_db_server('c', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_c', 'username', 'password');
add_db_server('d', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_d', 'username', 'password');
add_db_server('e', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_e', 'username', 'password');
add_db_server('f', 'dc1', 1, 1, 'dbserver.com', 'dbserver.com', 'wpmu_f', 'username', 'password');

//
//	Note: you can also place this section in a file called db-list.php in wp-content
//  EX: add_db_server('global', 'dc1', 1, 1,'global.mysql.example.com:3509','global.mysql.example.lan:3509', 'global-db', 'globaluser',  'globalpassword');
//------------------------------------------------------------------------//
//---VIP Blogs------------------------------------------------------------//
//------------------------------------------------------------------------//
//	Usage: add_vip_blog(BLOG_ID, DS)
//	EX: add_vip_blog(1, 'vip1');
// DN: I didn't add any VIP blogs.
?>

To create the databases, I used the script at http://db-tools.wpmudev.org/db.php and it generated the code below, which I ran on the MySQL server to create the databases:

CREATE DATABASE `wpmu_global` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_0` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_1` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_2` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_3` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_4` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_5` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_6` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_7` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_8` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_9` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_a` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_b` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_c` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_d` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_e` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE DATABASE `wpmu_f` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

After copying the db.php and db-config.php files into place as per Jim’s instructions, it all Just Worked™. New content was being stored in the 16 blog databases, and sites were behaving as expected, but with slightly less table explosion bloat as before.

One thing that makes me a little nervous is that the multi-db code isn’t core to WordPress, and is part of the premium.wpmudev.org subscription. This means that it can break in the future – there is no obligation for WordPress to continue to work with it, and if for some reason premium.wpmudev.org decides to abandon the plugin or stop updating it, I’m locked into WordPress 2.7. Neither of these made me lose too much sleep. Worst case scenario, I can always recombine the tables from all 17 databases back into a single überdatabase, assuming we haven’t outgrown the physical limits of a single MySQL database by then.

security hole in wordpress-admin-bar under WPMU?

I just tried logging into ucalgaryblogs.ca using a test user account, and was surprised to see a strange item in the admin bar at the top of the page:

wordpress-admin-bar-security-hole

I was curious, so I clicked it.

wordpress-admin-bar-security-hole-menu

mwah? Those are site-admin items, being displayed to a non-admin user. I was actually able to click the “Admin Message” item to set that, even though the logged in user wasn’t an admin. Scary. Luckily, nobody’s noticed the extra menu yet – or if they have, they’ve behaved.

I poked around in the wordpress-admin-bar.php file to see if I could plug the hole. I have no idea if this is the right way, but I’ve added this bit:

} else {
    if ($menu[0]['title'] === null)  continue; // this is the line I added
    echo '                  <li class="wpabar-menu_';
    if ( TRUE === $menu[0]['custom'] ) echo 'admin-php_';

It’s down around line 320 or so. It’s probably not the correct or most reliable way to strip that menu from non-admin-users’ version of the admin bar, but it worked. Here’s the result:

wordpress-admin-bar-security-hole-fixed

Has anyone else seen the extra menu? Could it have just been a freak thing only on my WPMU install, or is this a wide open potential security problem in the shipping wordpress-admin-bar.php file? It was written for non-WPMU WordPress, so it’s quite possible it just doesn’t grok the different types of users in WPMU.