Tag Archives: BuddyPress

Redirect BuddyPress activity reply links to forum’s “Leave a Reply”

Activity stream replies in BuddyPress are pretty cool, but they have the potential to be confusing. On the CUNY Academic Commons, we have disabled activity replies for activity entries related to blogs and forums, because allowing replies in these cases has the potential to confuse users and fracture conversation.

There are a number of ways that this could (and should, and will!) be improved in future versions of BuddyPress. But, for now, here’s a trick. The following code will change the behavior of the Reply buttons for forum-related activity entries (new forum topics, and forum topic replies), so that instead of sliding down the inline activity comment box, it goes to the Reply form on the forum topic itself.

Side note: This seems like it’d be an easy thing to do, but it turns out to be somewhat complex. As I explain in the inline documentation, the issue of pagination means that there’s no predictable way to easily concatenate a URL for a topic’s reply box (this is one of the things I want to fix in BP core) – you have to fetch the number of total replies and figure out the last page from there. Also, in the case of topic replies, you have to do an additional query to get the id of the topic that the post belongs to, because that info is not stored in the activity table. The function cac_insert_comment_reply_links() below tries to consolidate these lookups to add as few queries as possible to the pageload.

Second side note: This code is not particularly beautiful. It makes direct queries to the bbPress database tables. So sue me.

OK, so the code itself. First, put this chunk into your bp-custom.php file.

[code language=”php”]
/**
* Gets accurate reply URLs for the activity stream
*
* Getting accurate Reply links for forum topics is tricky because of pagination – you need to know
* how many total posts are in the topic so that you can figure out what the last page should be.
* Moreover, the forum reply activity items don’t have the topic_id stored with them. This function
* attempts to minimize DB queries by looking up all topic_ids at once, then looking up all post
* counts at once – adding 2 queries for the activity loop is better than 20.
*
* Todo: Get a real redirecter into BuddyPress itself
*/
function cac_insert_comment_reply_links( $has_comments ) {
global $activities_template, $wpdb, $bbdb;

do_action( ‘bbpress_init’ );

$topics_data = array();
$posts_data = array();
foreach( $activities_template->activities as $key => $activity ) {
if ( $activity->type == ‘new_forum_topic’ ) {
$topic_id = $activity->secondary_item_id;
$topics_data[$topic_id][‘url’] = $activity->primary_link;
$topics_data[$topic_id][‘activity_key’] = $key;
}

if ( $activity->type == ‘new_forum_post’ ) {
$post_id = $activity->secondary_item_id;
$posts_data[$post_id][‘url’] = array_pop( array_reverse( explode( ‘#’, $activity->primary_link ) ) );
$posts_data[$post_id][‘activity_key’] = $key;
}
}

// In cases where we only have the post id, we must do an extra query to get topic ids
if ( !empty( $posts_data ) ) {
$post_ids = array_keys( $posts_data );
$post_ids_sql = implode( ‘,’, $post_ids );
$sql = $wpdb->prepare( “SELECT topic_id, post_id FROM {$bbdb->posts} WHERE post_id IN ({$post_ids_sql})” );
$post_topic_ids = $wpdb->get_results( $sql );

// Now that we have the topic IDs, we can add that info to $topics_data for the main query
foreach( $post_topic_ids as $post_topic ) {
$topics_data[$post_topic->topic_id] = $posts_data[$post_topic->post_id];
}
}

// Now for the main event
// First, make a topic list and get all the associated posts
$topic_ids = implode( ‘,’, array_keys( $topics_data ) );
$sql = $wpdb->prepare( “SELECT topic_id, post_id FROM {$bbdb->posts} WHERE topic_id IN ({$topic_ids})” );
$posts = $wpdb->get_results( $sql );

// Now we get counts. BTW it sucks to do it this way
$counter = array();
foreach( $posts as $post ) {
if ( empty( $counter[$post->topic_id] ) )
$counter[$post->topic_id] = 1;
else
$counter[$post->topic_id]++;
}

// Finally, concatenate the reply url and put it in the activities_template
foreach( $topics_data as $topic_id => $data ) {
$total_pages = ceil( $counter[$topic_id] / 15 );
$reply_url = cac_forum_reply_url( $data[‘url’], $total_pages, 15 );
$key = $data[‘activity_key’];
$activities_template->activities[$key]->reply_url = $reply_url;
}

return $has_comments;
}
add_action( ‘bp_has_activities’, ‘cac_insert_comment_reply_links’ );

/**
* Filters the url of the activity reply link to use reply_url, if present
*/
function cac_filter_activity_reply_link( $link ) {
global $activities_template;

if ( !empty( $activities_template->activity->reply_url ) )
return $activities_template->activity->reply_url;
else
return $link;
}
add_action( ‘bp_get_activity_comment_link’, ‘cac_filter_activity_reply_link’ );

/**
* Echoes the proper CSS class for the activity reply link. This is necessary to ensure that
* the JS slider does not appear when we have a custom reply_url.
*/
function cac_activity_reply_link_class() {
global $activities_template;

if ( !empty( $activities_template->activity->reply_url ) )
echo ‘class=”acomment-reply-nojs”‘;
else
echo ‘class=”acomment-reply”‘;
}

/**
* A replacement for bp_activity_can_comment(). Todo: deprecate into a filter when BP 1.3 comes out
*/
function cac_activity_can_comment() {
global $activities_template, $bp;

if ( false === $activities_template->disable_blogforum_replies || (int)$activities_template->disable_blogforum_replies ) {
// If we’ve got a manually created reply_url (see cac_insert_comment_reply_links(), return true
if ( !empty( $activities_template->activity->reply_url ) )
return true;

if ( ‘new_blog_post’ == bp_get_activity_action_name() || ‘new_blog_comment’ == bp_get_activity_action_name() || ‘new_forum_topic’ == bp_get_activity_action_name() || ‘new_forum_post’ == bp_get_activity_action_name() )
return false;
}

return true;
}
[/code]

You’ll note that there are a few places in that code where the number 15 is mentioned explicitly. I’m assuming that you’re using 15 posts-per-page for your single topic pagination. You can change this number accordingly if you want.

Next, you’ll have to make a few changes in your theme’s activity/entry.php to account for the changes. There are two relevant changes. First, you’ll be removing the activity reply button’s CSS class (hardcoded by default) and replacing it with the dynamically generated version in cac_activity_reply_link_class(). Second, you’ll be swapping out the checks for bp_activity_can_comment() with cac_activity_can_comment(), so that you can still block blog-activity comments. The code below is lines 27-29 of my activity/entry.php – you should be able to figure out which lines to replace with the following, as I haven’t changed much.

[code language=”html”]

()

[/code]

Finally, because you’ve changed the CSS selector on some of the reply buttons, you’ll want to add some styles to your stylesheet. These are borrowed right from bp-default.

[code language=”css”]
.activity-list div.activity-meta a.acomment-reply-nojs {
background: #fff9db;
border-bottom: 1px solid #ffe8c4;
border-right: 1px solid #ffe8c4;
color: #ffa200;
}

div.activity-meta a.acomment-reply-nojs:hover {
background: #f7740a;
color: #fff;
border-color: #f7740a;
}
[/code]

Good luck!

New BuddyPress plugin: BuddyPress Docs

BuddyPress Docs edit screen

BuddyPress Docs edit screen

Today I am releasing the first public beta of a significant new BuddyPress plugin: BuddyPress Docs. BuddyPress Docs is a collaborative, front-end, rich-text, document editing tool for BuddyPress groups (individual user Docs are an upcoming feature). Read much more about the plugin’s features.

This plugin has been developed for the CUNY Academic Commons (though it won’t be live there for a few weeks). Today’s release is a pre-stable beta – it should run fine, but there are bound to be bugs, and you probably won’t want to run it on a production site quite yet.

You can follow the plugin’s development at http://github.com/boonebgorges/buddypress-docs.

Invite Anyone 0.8 and CloudSponge integration

Version 0.8 of my popular Invite Anyone plugin for BuddyPress has a brand new feature: optional integration with the CloudSponge service. CloudSponge is a service that acts as a front-end for a number of API address-book services (at the moment, Gmail, Yahoo Mail, Windows Live/Hotmail/MSN, AOL, Plaxo, the Mac Address Book, and Microsoft Outlook). With CloudSponge turned on alongside Invite Anyone, members of your BuddyPress community can invite friends to join the site by dipping into their preferred address book and selecting the desired email addresses from an easy-to-use interface. This can be a powerful driver of membership and engagement. To sign up for CloudSponge and turn on IA integration, go to the Invite Anyone Dashboard panel (be sure to look in the Network Admin on 3.1 Multisite!).

CloudSponge is a paid service (though they have a free trial). If you decide to become a CloudSponge customer through Invite Anyone, I’ll receive a small portion of your membership fees as an affiliate bonus. You can think of this as a way to support future development on Invite Anyone, by making sure that I have money to purchase the beer and pizza that fuel so much of my code 🙂

Of course, CloudSponge is an optional add-on to Invite Anyone. The plugin itself, and all its existing features, will continue to be free. CloudSponge is a little something extra for those communities where a regular stream of new members is key to success.

Hardening BuddyPress Group Documents

The BuddyPress Group Documents plugin allows groups a handy way for users to share documents with fellow members of a BP group. It’s crucial to the work that is done on the CUNY Academic Commons. But, by default, the plugin stores documents in a subdirectory of your WP uploads folder (usually /wp-content/blogs.dir/ on multisite). That means that documents are available directly, to anyone who has the URL, regardless of the public/private/hidden status of groups. This isn’t a problem from within BuddyPress, since URLs for the documents only appear inside of the protected group interface. But if the URL is shared, then the document becomes publicly available. Worse, if someone posts the URL of a document in a public place, search engine bots will find it, and the contents of the document could end up in Google.

I wrote a few helper functions to change this behavior. The strategy is this: Move the files so that they are not accessible via URL, ie in a directory above the web root. (In my case, it’s a directory called bp-group-documents, just above my web root.) Then, catch requests of a certain type (I’ve chosen to go with a URL parameter get_group_doc=), and check them to see whether the current user has the adequate permission to access the document in question. Finally, make sure that all of the URLs and paths that BPGD uses to upload and display documents are filtered to the updated versions. I’ve provided my code below – use and modify at your pleasure. You should be able to place it in your plugins/bp-custom.php file, and then move your existing docs from their current location (probably something like wp-content/blogs.dir/1/files/group-documents) to the new directory.

I also added a line to my .htaccess file to ensure that requests to the old URLs are redirected to the new, hardened URL. That line is this:
[code]
RewriteRule ^wp-content/blogs.dir/1/files/group-documents/(.*) /?get_group_doc=$1 [R,L]
[/code]
Obviously, you may have to modify it for different file paths.

EDITED Feb 8, 2011 to include the code for creating directories when none exist

[code language=”php”]
define( ‘BP_GROUP_DOCUMENTS_SECURE_PATH’, substr( ABSPATH, 0, strrpos( rtrim( ABSPATH, ‘/’ ), ‘/’ ) ) . ‘/bp-group-documents/’ );

function cac_filter_doc_url( $doc_url, $group_id, $file ) {
$url = bp_get_root_domain() . ‘?get_group_doc=’ . $group_id . ‘/’ . $file;
return $url;
}
add_filter( ‘bp_group_documents_file_url’, ‘cac_filter_doc_url’, 10, 3 );

function cac_filter_doc_path( $doc_url, $group_id, $file ) {
$document_dir = BP_GROUP_DOCUMENTS_SECURE_PATH . $group_id . ‘/’;

if ( !is_dir( $document_dir ) )
mkdir( $document_dir, 0755, true );

$path = BP_GROUP_DOCUMENTS_SECURE_PATH . $group_id . ‘/’ . $file;
return $path;
}
add_filter( ‘bp_group_documents_file_path’, ‘cac_filter_doc_path’, 10, 3 );

function cac_catch_group_doc_request() {
$error = false;

if ( empty( $_GET[‘get_group_doc’] ) )
return;

$doc_id = $_GET[‘get_group_doc’];

// Check to see whether the current user has access to the doc in question
$file_deets = explode( ‘/’, $doc_id );
$group_id = $file_deets[0];
$group = new BP_Groups_Group( $group_id );

if ( empty( $group->id ) ) {
$error = array(
‘message’ => ‘That group does not exist.’,
‘redirect’ => bp_get_root_domain()
);
} else {
if ( $group->status != ‘public’ ) {
// If the group is not public, then the user must be logged in and
// a member of the group to download the document
if ( !is_user_logged_in() || !groups_is_user_member( bp_loggedin_user_id(), $group_id ) ) {
$error = array(
‘message’ => sprintf( ‘You must be a logged-in member of the group %s to access this document. If you are a member of the group, please log into the site and try again.’, $group->name ),
‘redirect’ => bp_get_group_permalink( $group )
);
}
}

// If we have gotten this far without an error, then the download can go through
if ( !$error ) {

$doc_path = BP_GROUP_DOCUMENTS_SECURE_PATH . $doc_id;

if ( file_exists( $doc_path ) ) {
$mime_type = mime_content_type( $doc_path );
$doc_size = filesize( $doc_path );

header(“Cache-Control: public, must-revalidate, post-check=0, pre-check=0”);
header(“Pragma: hack”);

header(“Content-Type: $mime_type; name='” . $file_deets[1] . “‘”);
header(“Content-Length: ” . $doc_size );

header(‘Content-Disposition: attachment; filename=”‘ . $file_deets[1] . ‘”‘);
header(“Content-Transfer-Encoding: binary”);
ob_clean();
flush();
readfile( $doc_path );
die();

} else {
// File does not exist
$error = array(
‘message’ => ‘The file could not be found.’,
‘redirect’ => bp_get_group_permalink( $group ) . ‘/documents’
);
}
}
}

// If we have gotten this far, there was an error. Add a message and redirect
bp_core_add_message( $error[‘message’], ‘error’ );
bp_core_redirect( $error[‘redirect’] );
}
add_filter( ‘wp’, ‘cac_catch_group_doc_request’, 1 );

// http://www.php.net/manual/en/function.mime-content-type.php#87856
if(!function_exists(‘mime_content_type’)) {

function mime_content_type($filename) {

$mime_types = array(

‘txt’ => ‘text/plain’,
‘htm’ => ‘text/html’,
‘html’ => ‘text/html’,
‘php’ => ‘text/html’,
‘css’ => ‘text/css’,
‘js’ => ‘application/javascript’,
‘json’ => ‘application/json’,
‘xml’ => ‘application/xml’,
‘swf’ => ‘application/x-shockwave-flash’,
‘flv’ => ‘video/x-flv’,

// images
‘png’ => ‘image/png’,
‘jpe’ => ‘image/jpeg’,
‘jpeg’ => ‘image/jpeg’,
‘jpg’ => ‘image/jpeg’,
‘gif’ => ‘image/gif’,
‘bmp’ => ‘image/bmp’,
‘ico’ => ‘image/vnd.microsoft.icon’,
‘tiff’ => ‘image/tiff’,
‘tif’ => ‘image/tiff’,
‘svg’ => ‘image/svg+xml’,
‘svgz’ => ‘image/svg+xml’,

// archives
‘zip’ => ‘application/zip’,
‘rar’ => ‘application/x-rar-compressed’,
‘exe’ => ‘application/x-msdownload’,
‘msi’ => ‘application/x-msdownload’,
‘cab’ => ‘application/vnd.ms-cab-compressed’,

// audio/video
‘mp3’ => ‘audio/mpeg’,
‘qt’ => ‘video/quicktime’,
‘mov’ => ‘video/quicktime’,

// adobe
‘pdf’ => ‘application/pdf’,
‘psd’ => ‘image/vnd.adobe.photoshop’,
‘ai’ => ‘application/postscript’,
‘eps’ => ‘application/postscript’,
‘ps’ => ‘application/postscript’,

// ms office
‘doc’ => ‘application/msword’,
‘rtf’ => ‘application/rtf’,
‘xls’ => ‘application/vnd.ms-excel’,
‘ppt’ => ‘application/vnd.ms-powerpoint’,

// open office
‘odt’ => ‘application/vnd.oasis.opendocument.text’,
‘ods’ => ‘application/vnd.oasis.opendocument.spreadsheet’,
);

$ext = strtolower(array_pop(explode(‘.’,$filename)));
if (array_key_exists($ext, $mime_types)) {
return $mime_types[$ext];
}
elseif (function_exists(‘finfo_open’)) {
$finfo = finfo_open(FILEINFO_MIME);
$mimetype = finfo_file($finfo, $filename);
finfo_close($finfo);
return $mimetype;
}
else {
return ‘application/octet-stream’;
}
}
}

[/code]

Group Announcements tab in BuddyPress

Cross-posted at the CUNY Academic Commons Development blog

I had a request or two to explain how I built the Group Announcements feature on the CUNY Academic Commons. Here goes.

Brief background: When the Commons was upgraded to BuddyPress 1.2, we got the benefit of interactive activity streams everywhere, including groups. This caused some confusion, however, as users were uncertain where conversation best fit into the Commons’s architecture: in the Forums (where it had been traditionally), or in the activity stream. In some communities this kind of fracturing might be okay or even welcome, but in ours it was confusing. At the same time, we wanted a way for group admins and mods to send important notices to the members of their groups. By taking the group activity updates and repurposing it as a Group Announcements section, I was able to kill two birds with one stone: providing an announcement space for mods, while focusing extended discussion in the forums.

You can download the CAC Group Announcements plugin here.

I’m not putting this in the repo at the moment because I don’t want to build a proper admin UI and support it 🙂 For that reason, here is a primer on how the plugin works – if you want to customize or maintain it, you’re on your own, buster.

  1. The CAC_Group_Announcements class is an instance of the BuddyPress Group Extension API. It is responsible for creating the Announcements tab markup and adding it to the nav. You’ll notice that the majority of the markup is created by including bp-default’s activity-loop.php and post-form.php templates. You could customize this more if you wanted.
  2. bp_is_group_announcements() is a little template tag that can be used to test whether you’re looking at a group announcements page. This is needed for the activity filter, in step 3.
  3. cac_set_announcement_filter() adds a filter to the bp_has_activities query string when you are looking at an announcements page, so that it only displays activity items of the type activity_update. In other words, when you are looking at the regular activity stream for the group, you see all of the associated group activity items (new members, forum posts, etc) but when you’re on the announcements page you only see activity updates (which, remember, have been repurposed as announcements).

As I look at the code, I see that there are things I would definitely change if I were going to make this into a distributed plugin. If you want to make those changes, be my guest. You’re welcome to help each other in the comment section, but I won’t be formally supporting this, as it is a very basic hack that should happen at the theme level anyway. Good luck!

Looking back at 2010

2010 was a wild year for me, one that I’ll look back on as a turning point in my professional and personal life. For that reason I thought I might take stock of the past year. (Here’s 2009’s post.) If you are one of those snobs who think that year-end retrospectives are schlocky, feel free to get the hell out of my blog.

As 2010 opened, I was working full-time as the educational technologist by Queens College. I believed strongly (and continue to believe) in the importance of the work I was doing there, but I already knew a year ago that I wouldn’t be able to stay at the job for much longer. I identified as an ed tech, and part of the (really great) ed tech community, but it was a label that never really felt right. When people asked what I did for a living, I hesitated. I left the job near the end of May.

Since then, I have been supporting myself doing custom web development, almost exclusively using BuddyPress. In the last six months, I’ve transitioned from an uneasy edtech to a confident (though still n00bish in many ways) developer. It’s a classification that feels better in many ways. Moving into development has allowed me to be personally productive in ways that the structures of my old career simply couldn’t support. I produce a lot of software that is used by a lot of people; moreover, I am moving toward a position where I get to select only those projects that are of independent interest to me. Measured like this, 2010 was the most productive year of my life, made possible by the career move (and the new self-identification that came with it).

My move into development is not without misgivings. As an educational technologist, working in the confines of a traditional university, there were always connections (sometimes tenuous, but always discernable) between my day job and my identity as a graduate student. Granted, in the time I was at Queens – first as a graduate fellow and then as a full-timer – I made next to no progress on my dissertation. But the fact that I was in a university, and enabling teaching and learning in a hands-on way, kept me in constant communication with my inner philosopher: drawing on my teaching experience, speaking in academic tones with faculty members, engaging in debates on the goals and methods of educational technology in ways that never strayed far from the kinds of discourse I learned in the seminar room. My work as a developer, in contrast, is much less explicitly academic; while some of my projects (notably, the CUNY Academic Commons) have sustained my contact with the university, mostly I am paid to think about software and websites rather than anything else. In the short term, this will undoubtedly be a good thing – I attribute the progress I’ve made on my thesis in the semester since I left Queens College to the fact that my day job provides me with some much-needed release from the mental anguish of the university life. But the more I make a name for myself as a developer, where ‘developer’ is unqualified by ‘academic’ or any similar modifier, the more I have to make conscious decisions about how (and whether) I want my paying gigs to connect with my academic interests. It’s an issue I’ll continue to wrestle with in 2011.

Paralleling my move into a development career has been an increased participation in the WordPress world. In July I was made a moderator on the buddypress.org support forums. In October, I was brought on as a committing developer for the BuddyPress project. I spoke dozens of times through 2010 on WordPress and BuddyPress, at WordCamps, meetups, conferences, THATCamps, and various other fancy places. At the beginning of 2010 I felt like I’d staked out a position on the outskirts of the WordPress community; at the end of 2010, I feel like I’m much closer to its center. And while I could live without the occasional drama, tunnel-vision, and personality cultishness of some WordPressophiles, for the most part it has been a real treat getting to know, and getting to work with, so many of the best WP developers. It’s broken me out of that other echo chamber I come from (academia), made me a much better coder, and introduced me to some really fabulous folks.

In 2010, I also got more and more tangled up with the digital humanities community. In July, I spent a week at the Center for History and New Media for the One Week | One Tool project, where I was on a team that built Anthologize. I attended a number of THATCamps and was witness to a number of Twitter arugments of truly epic proportions. And while I could live without the occasional drama, tunnel-vision, and personality cultishness of some DigitalHumanitiesophiles, for the most part it has been a real treat getting to know, and getting to work with, so many of the best digital humanists. (Is there an echo in here?) My intellectual connection with DH is such that it is hard for me not to put scare quotes around ‘digital humanities’ every time I write it: I am an academic, and I do extensive work with digital technology, but the connection between the two is not manifest in my own work. Still, DH in 2010 has been an exciting place to locate oneself, with cool projects, smart people, and the occasional Big Idea rising to the top over the course of the year.

I continued being a dork in 2010. I came in 66th at the American Crossword Puzzle Tournament (breaking 50 in 2011! You read it here first!). I switched from QWERTY to Dvorak. I visited the Googleplex. I wrote a lot about pizza and barbecue. I made the decision to stop buying Apple products. I completed Angry Birds. I wrote 45 blog posts on Teleogistic, with a smattering of posts elsewhere. Teleogistic got 960 comments. I wrote many tens of thousands of lines of code, much of which was terrible, and much of which is sadly hidden forever on client servers, but some of which is free and helpful to many.

On June 5, 2010, I got married. I mention this last not because it is the least important event of the year but because it is the most. The process of preparing for a wedding, with the help and support of so many friends and loved ones, was something I will never forget. The wedding day was the most perfect day I can remember. And the girl I married – well, duh, she is the best part of 2010, or of any year.

The changes of 2010 were more significant for me than any year since I was in college. Nearly all of those changes have been for the better. I have some exciting plans for 2011, but for now I am happy to reflect on the year that was. For me, it was a good one.

Wildcard email whitelists in WordPress and BuddyPress

Cross-posted on the CUNY Academic Commons Development Blog

WordPress (and before that WPMU) has long had a feature that allows admins to set a whitelist of email domains for registration (Limited Email Registration). On the Commons, we need to account for a lot of different domains, some of which are actually dynamic – but they are all of the form *.cuny.edu. WP doesn’t support this kind of wildcards, but we’ve got it working through a series of customizations.

These first two functions form the heart of the process. The first one hooks to the end of the BP registration process, looks for email domain errors, and then sends the request to the second function, which does some regex to check against the wildcard domains you’ve specified. This is BP-specific, but I think you could make it work with WPMS just by changing the hook name.


function cac_signup_email_filter( $result ) {
	global $limited_email_domains;

if ( !is_array( $limited_email_domains ) )
		$limited_email_domains = get_site_option( 'limited_email_domains' );

$valid_email_domain_check = cac_wildcard_email_domain_check( $result['user_email'] );

if( $valid_email_domain_check ) {
		if ( isset( $result['errors']->errors['user_email'] ) )
			unset( $result['errors']->errors['user_email'] );
	}

return $result;
}
add_filter( 'bp_core_validate_user_signup', 'cac_signup_email_filter', 8 );

function cac_wildcard_email_domain_check( $user_email ) {
	global $limited_email_domains;

if ( !is_array( $limited_email_domains ) )
		$limited_email_domains = get_site_option( 'limited_email_domains' );

if ( is_array( $limited_email_domains ) && empty( $limited_email_domains ) == false ) { 
		$valid_email_domain_check = false;
		$emaildomain = substr( $user_email, 1 + strpos( $user_email, '@' ) );
		foreach ($limited_email_domains as $limited_email_domain) {
			$limited_email_domain = str_replace( '.', '.', $limited_email_domain);        // Escape your .s
			$limited_email_domain = str_replace('*', '[-_.a-zA-Z0-9]+', $limited_email_domain);     // replace * with REGEX for 1+ occurrence of anything
			$limited_email_domain = "/^" . $limited_email_domain . "/";   // bracket the email with the necessary pattern markings
			$valid_email_domain_check = ( $valid_email_domain_check or preg_match( $limited_email_domain, $emaildomain ) );
		}
	}

return $valid_email_domain_check;
}

Before WP 3.0, this was enough to make it work. The latest WP does increased sanitization on the input of the limited_email_domains field, however, which makes it reject lines like *.cuny.edu. The following functions add an additional field to the ms-options.php panel that saves the limited domains without doing WP’s core checks. (Beware: bypassing WP’s checks like this means that there are no safeguards in place for well-formedness. Be careful about what you type in the field, or strange things may happen.)


function cac_save_limited_email_domains() {
	if ( $_POST['cac_limited_email_domains'] != '' ) {
		$limited_email_domains = str_replace( ' ', "n", $_POST['cac_limited_email_domains'] );
		$limited_email_domains = split( "n", stripslashes( $limited_email_domains ) );

$limited_email = array();
		foreach ( (array) $limited_email_domains as $domain ) {
				$domain = trim( $domain );
			//if ( ! preg_match( '/(--|..)/', $domain ) && preg_match( '|^([a-zA-Z0-9-.])+$|', $domain ) )
				$limited_email[] = trim( $domain );
		}
		update_site_option( 'limited_email_domains', $limited_email );
	} else {
		update_site_option( 'limited_email_domains', '' );
	}
}
add_action( 'update_wpmu_options', 'cac_save_limited_email_domains' );

function cac_limited_email_domains_markup() {
	?>

<h3><?php _e( 'Limited Email Domains That Actually Work' ); ?></h3>

<table class="form-table">
	<tr valign="top">
		<th scope="row"><label for="cac_limited_email_domains"><?php _e( 'Limited Email Registrations' ) ?></label></th>
		<td>
			<?php $limited_email_domains = get_site_option( 'limited_email_domains' );
			$limited_email_domains = str_replace( ' ', "n", $limited_email_domains ); ?>
			<textarea name="cac_limited_email_domains" id="limited_email_domains" cols="45" rows="5">
			<br />
			<?php _e( 'If you want to limit site registrations to certain domains. One domain per line.' ) ?>
		</td>
	</tr>
	</table>

<?php
}
add_action( 'wpmu_options', 'cac_limited_email_domains_markup' );

New BuddyPress plugin: BP Group Reviews

I’m working on a project that needs to allow users to leave reviews of groups, kinda like buddypress.org does with plugins. So, with Andy’s permission, I cleaned up, packaged, and extended his code into a new plugin, BP Group Reviews. It allows users to leave a star rating and a text review of any group on your BuddyPress installation.

Lessons from the Google Summer of Code Mentor Summit

A few quick thoughts about the Google Summer of Code Mentor Summit, which I attended last weekend.

Google Summer of Code is a program, run by Google, which encourages open source development by paying college students to undertake summer coding projects with various open source projects. I co-mentored two projects for WordPress, and was one of the lucky few from among WP’s fifteenish mentors to get a trip to the Googleplex in Mountain View.

The Summit was organized as an unconference. On Saturday, attendees proposed session topics on a big scheduling board, and indicated their interest in other suggested sessions with stickers. This being a supremely geeky conference, I didn’t understand about half of the session titles.

A few takeaways, in no particular order:

  • Process matters. A lot. Probably 2/3 of the sessions I attended were devoted to project workflow: version control, code review, various kinds of testing. Probably some of the focus on process is due to the fact that it constitutes common ground between even those individuals whose software projects are quite different from each other. But I think it also speaks to the importance of workflow that really works, especially in the decidedly non-top-down world of open-source development communities.
  • WordPress seems pretty far behind the curve in terms of development infrastructure. Take version control: WordPress is the only project I heard about all weekend that still uses (or is not in the process of moving away from) centralized version control like Subversion. Git seemed like the most popular platform (there was a whole session on migrating massive project repos from SVN to Git, which was probably my favorite session of the weekend). I came away with lots of ideas for how the WP and BuddyPress development processes might be improved (and, more importantly, why it might be worthwhile to pursue these ideas), which I’ll be working on in the upcoming weeks and months.
  • More generally, I came away realizing that WordPress devs (and probably other kinds of devs, but this is what I know!) have a lot to learn from the way that similar software development projects are run. I was part of some extremely interesting conversations with core developers from Drupal and TYPO3 and was really, really impressed with the way the way that their development workflow informs and enables better software. Some WordPress fans have a tendency (sometimes joking, sometimes not) to disparage other projects like this, an attitude that can prevent us from learning a lot from each other. That’s a real shame, and it’s something I’d like to rail against.

I met some great people and learned a lot at the Summit. Many thanks to Google for footing the bill, to WordPress for selecting me to go, and to Stas and Francesco for their cool GSoC projects!