Author Archives: Boone Gorges

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]

New WordPress plugin: Prezi WP

I had a request to allow Prezis to be embedded on the CUNY Academic Commons, but the one plugin I tried for that purpose seemed to be broken and overengineered. So I took an hour and wrote my own: Prezi WP. In brief, it gives you a [prezi] shortcode for easy embedding of those Mind Blowing, Non-Linear bad boys.

It’ll be in the wordpress.org repository soon enough, but for now you can read more and download it here.

Planning an Introduction to Philosophy course

In April, I’ll be teaching an Introduction to Philosophy course at my alma mater, Cornell College. (Cornell’s academic calendar is called One Course at a Time, or the “block plan”, so that an entire course happens in the confines of three-and-a-half weeks. Thus the ‘In April’ bit.) I am really excited to be teaching at my very small and very dear Cornell, but I am nervous about the class itself.

I find the very notion of an “introduction to philosophy” course to be slippery and intractable. Philosophy is not like, say, Russian, where the first introductory course provides the pieces of knowledge (vocabulary, conjugation, stuff like that) that will be required in all subsequent courses. Philosophy has its own techniques and terms of art, of course, but they’re not the sorts of things that lend themselves naturally to sequential teaching, like in a foreign language (or math, or chemistry….).

The term ‘introduction’ seems apt in the case of philosophy, because the act of putting together and teaching an intro to philosophy course seems very much like the act of introducing more generally. If I want to introduce a visitor to Brooklyn, for example, I have to decide which of the things that I know about Brooklyn (which are too many to share in a short visit!) are salient enough, pleasant enough, relevant enough to include. The calculus depends not only on my knowledge of Brooklyn (the introducee) but also the visitor (the introduced). I want the picture of Brooklyn they get from my visit to reflect the way that I represent Brooklyn to myself, or at least a sort of idealized version of my own idea that will leave the visitor with pleasant memories of the city and a desire to learn more. And how I aim to develop that picture will change based on what I know about the visitor and his background knowledge.

So the trick is to do something similar with philosophy. The problem is that – like Brooklyn! – philosophy is vast and deep. Moreover (and here is where the analogy breaks down), in the case of philosophy it actually matters that the introduced comes away with a truly representative sense of what philosophy is like – at least, that is, insofar as the work of philosophy is independantly important. How do you select a reading list that is representatively broad without being vapid? sufficiently deep to represent the way philosophy is done without giving an overly narrow perception of the philosophical landscape? simplified enough to be approachable without overly caricaturing? relevant enough to the existing interests and knowledge of underclassmen without being pandering? And, given that there is no “right” way of making these decisions, how do you weigh each of these factors in the inevitable compromise that is represented by the eventual syllabus?

For me, it’s helpful to think about the different indexes one could use to organize and shape one’s course. These indexes are not mutually exclusive, but they must necessarily be ordered – favoring one index means making another index secondary, etc. Also, some of these indexes might necessarily be nested within others.

  • Chronology (eg 18th century)
  • Nationality/language
  • Great texts
  • Philosopher biography
  • Philosophical schools (eg empiricism, or utilitarianism)
  • Philosophical subfields (eg epistemology, or philosophy of language)
  • Topics (eg abortion, personal identity, truth)
  • Questions (eg ‘What is the nature of justice?’)

There are surely more, and different ways of carving them up, but this is a starting point.

In the past, when I have taught intro, my primary index has been philosophical subfields, with secondary index of questions and tertiary index of Great Texts. Thus I might do a unit on the philosophy of religion, which contains a section on proofs for the existence of God and a section on the problem of evil. In each of those sections, we read very well-known texts that represent different kinds of answers to the questions at hand. The next unit might be on ethics, with sections on virtues and on the goodness of acts, where we read Aristotle and Mill and Kant. And so on.

Clearly, this is not a terribly creative way to teach an intro class. From what I can remember, it mirrors closely the way that intro was laid out when I took it many years ago. But even though it is probably a pretty common approach, it is in many respects quite arbitrary. I try to take the Indian Buffet method – give just a taste of a couple different kinds of dish, and even the terrified diner might find something to latch onto. But the variety that constitues the strength of the buffet approach is also a weakness, as it threatens to give the newcomer an impression that the discipline is really a jumble of neato but otherwise unrelated questions. This is not indicative of the way that real philosophers actually do their work; that this approach is shallow means ipso facto that it does not display the best of what philosophy has to offer.

For this reason I find myself very drawn to an approach that takes a question or a topic as its primary index. A mentor of mine recently told me that he taught his most recent intro class as a sort of seminar on the topic of death, which involved both the reading of philosophical Great Works with a focus on death, as well as the introduction of texts that might not often be seen on a typical intro syllabus. This approach does not do away with the issue of arbitrariness – why choose death, after all? – but it does bump it up to a global arbitrariness, where once the overall topic of the course has been decided, the relevance of each reading to a cohesive whole is manifest.

There are several problems with this kind of approach, though. First is the obvious one: what topic do I choose? Ideally it’d be something that would permit the inclusion of at least some Great Works, and with a broad enough appeal to non-philosophers to convince them that it’s worth studying. A more fundamental problem that nags at me is whether this approach to an intro course is justifiable. Every course I ever took that was pitched this way, around a single topic, was an upper-level seminar type course. Does an intro course have the responsibility to be broad in scope? Or is it perhaps possible to have an appropriately broad list of readings centered around a single topic? Or – and this is my secret suspicion – is the concept of an introduction to philosophy class vague enough to be more or less meaningless, so that just about any kind of legitimate philosophy course might be explained away as an “intro” under the right kinds of circumstances?

I’d like to hear what some of my philosopher friends think about this dilemma (or maybe lack of dilemma, if I’m way off base). I’m also curious to hear what happens in other disciplines where the concept of an “introductory” course is just as inscrutable.

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' );

Extending Anthologize: Part 2

In my last post, I gave a brief overview of what happens when you click the Export Project button in Anthologize, with an eye toward demonstrating the role that third party plugins (written by people Just Like You!) can play in extending the software. In this post, I’ll flesh out some of those details.

Step 1: Anthologize, meet my format

Anthologize has what I like to think of as two internal APIs: the WordPress-facing API, and the theming API. The first step is to let Anthologize know that your format exists, which you’ll do by using the key function in the WP-facing API: anthologize_register_format(). (Check out how the WPAPI functions work in the source.) You should call anthologize_register_format() in a function hooked to anthologize_init, which ensures that it won’t fire before Anthologize is ready for it. Here’s a simple example:
[code language=”php”]
function register_my_anthologize_format() {
$name = ‘example_format’;
$label = ‘My Anthologize Format’;
$loader_file = dirname(__FILE__) . ‘/base.php’;

anthologize_register_format( $name, $label, $loader_file );
}
add_action( ‘anthologize_init’, ‘register_my_anthologize_format’ );
[/code]

anthologize_register_format_option() takes three arguments. The first is the name that Anthologize will use internally as a unique key for your format. The second is the name that users will see on the Export screen radio button and elsewhere in the interface. (It’s a good idea to set a textdomain for your strings so that they can be translated – eg $label = __( 'My Anthologize Format', 'my-anthologize-plugin' );.) The third argument is the absolute server path of your main translator file, the file that will be loaded at export time.

You can read a more in-depth example, embedded in a loader class, in this bare-bones plugin that I built for testing purposes. And the definitive examples, of course, can be found in the way that Anthologize registers its native export formats (yum, we love eating our own dog food!).

What does this give you? First, it makes your format available as a format option on the second screen of the export process:



Second, it gives your format a Publishing Options page. Since we haven’t registered any options yet, we get the bare minimum, the shortcode toggle that all formats get:



Third – and this is the big thing – anthologize_register_format() tells Anthologize that, if the user has selected your format on the export screens, the TEI document containing the project data will be handed off to the translator file whose path you specified when you registered the format. That’s where the format translation itself happens.

Step 2: Gimme some options

One of the big thoughts behind the Anthologize plugin architecture is to model WordPress’s design philosophy, which is to provide relatively few options in the core product but to allow for arbitrarily extended functionality by plugins. That’s the purpose behind anthologize_register_format_option(), the second pillar of Anthologize’s WP-facing API. anthologize_register_format_option() gives a framework for plugins to register any kind of option associated with their custom format, provides all the integrated markup for presenting those options to users, and passes along the user’s choices to the Anthologize TEI document used for format translation.

Format options can also be loaded at anthologize_init, though you should be careful to load them after the format itself. I’ll do that by using an add_action priority:
[code language=”php”]
function register_my_anthologize_format_options() {
$website_name = ‘website’;
$website_label = ‘Website’;
$website_type = ‘textbox’;

anthologize_register_format_option( ‘example_format’, $website_name, $website_label, $website_type );
}
add_action( ‘anthologize_init’, ‘register_my_anthologize_format_options’, 15 );
[/code]
(Note that the first argument is the unique $name of the format I intend to associate the option with.) This registers a plain textbox with the label ‘Website’:



You can also register checkboxes, if you need a Boolean:
[code language=”php”]
$option_name = ‘include_dedication’;
$option_label = ‘Include Dedication?’;
$option_type = ‘checkbox’;

anthologize_register_format_option( ‘example_format’, $option_name, $option_label, $option_type );
[/code]


Or you can register dropdowns:
[code language=”php”]
$fontface_format_name = ‘example_format’;
$fontface_option_name = ‘font-face’;
$fontface_label = ‘Font Face’;
$fontface_type = ‘dropdown’;
$fontface_values = array(
‘courier’ => ‘Courier’,
‘georgia’ => ‘Georgia’,
‘garamond’ => ‘Garamond’
);
$fontface_default = ‘georgia’;

anthologize_register_format_option( $fontface_format_name, $fontface_option_name, $fontface_label, $fontface_type, $fontface_values, $fontface_default );
[/code]
This kind of option highlights some of the other possible arguments that anthologize_register_format_option() will take: an array of possible values, and a default selected value:


All the options you provide, and in particular the options selected by the user at export time, are handed to your main translator file at the very end of the Anthologize export process.

So that’s how you get your format and its options registered on the WordPress side of things. Stay tuned for a discussion of how translators might be built.

Are you a potential plugin developer with ideas about how this API might evolve to better suit your purposes? Hop onto the dev list.

Extending Anthologize: Part 1

Anthologize has seen two version releases since its initial launch in August. Much of the progress since then (aside from some smallish – but needed – features and bugfixes) has been centered on the development of a plugin architecture for Anthologize, a system that will allow individuals to build custom output formats for their Anthologize content in a relatively straightforward way. Anthologize development team member (and metadata badass) Patrick Murray-John has been hard at work on this project: creating prototype plugins, writing blog posts about the plugin process, and of course building large parts of API itself. In this series of posts, I’d like to augment some of Patrick’s specific musings with a general explanation of how Anthologize works, and how the plugin API lets you tap into it. Think of it as an introduction to Anthologize plugin building.

First, a brief peek under the hood. Anthologize’s tagline is “Use the power of WordPress to transform online content into an electronic book.” How does Anthologize take you from point A – your WordPress content – to point B – your ebook? When you press the Export button on the final Export Project screen, you set into motion a two-step process.

Step 1: WordPress to TEI(ish)

WordPress content is stored in the wp_posts database table, and is typically accessed using WP’s “loop”, which is a set of iterative and indexical functions that make it easy to get and display post data in whichever way you’d like. When you hit the final Export button, Anthologize catches the form submit and hands things off to the format translator, which will get the content out of the database. We’ll use PDF as our example. The main translator file is base.php. That file acts like a top-level manager for all the things that have to be done in order for PDFs to be generated. The first important thing that any export format has to do is to call up the content of the project, which it does by instantiating the TeiDom class.

In many ways, TeiDom is the workhorse of Anthologize. Using the session data passed to it from the PDF base.php file – data that includes the project id, the desired page size, the content of the dedication, stuff like that – it uses a variation on The Loop to collect each part and item from an Anthologize project. Those objects, along with their metadata, are then fed into an empty TEI template file before getting handed back to the individual export format translator.

Why the middleman? Early in the development process, the team made a few decisions about the way that Anthologize ought to operate. First, though it’s currently being developed as a WordPress plugin, we should anticipate a time when much of Anthologize could be ported to another CMS or to a standalone application. If format translators like PDF had to dig directly into the WP database, or were forced to use The Loop to get project data, then they’d have to be refactored when the data lived somewhere other that WP. TEI is a platform-independent format, and since format translators like the PDF generator communicate only with the TeiDom (not directly with WordPress), they should be fairly platform-independent as well. Second, if we were going to have a middleman, we wanted it to be one with extremely broad expressive power, and one for which standards and translation techniques already exist. By choosing TEI, we open the door for armies of archivists, librarians, and other such format wizards, armed with XSLT ninjitsu. (In fact, that’s how Anthologize dev team member Patrick Rashleigh built the Anthologize epub generator!)

In the header for this section, I say TEI(ish) rather than TEI. That’s because the Anthologize middleman TEI layer is not the kind of TEI that your local text-encoder might expect. In particular, the content of your WordPress blog posts, which is already marked up with HTML, is untouched in the export process. A true TEI markup of your text would mean lots of manual encoding, so we just pass it along as-is. This untouched HTML post content, however, is embedded in a larger TEI framework for holding the metadata and generally explaining the structure of the project document. It’s not necessarily the kind of document you could use to build a richly marked-up text visualization, but it works well for the purposes of simple presentation.

Step 2: TEI to your format

Once the format generator (remember our friend templates/pdf/base.php?) gets the TEI document back from the TeiDom workhorse – which is essentially shared by all Anthologize export formats – the format-specific work can begin. PDF uses its own custom TEI-to-PDF class, along with an included PDF generation library, to parse the TeiDom object and turn it into something that, when delivered to your browser, is understood as a PDF. This, of course, is the hard part of building a translator, and is very format-specific.

The cool part about this part of the process, though, is the amount of flexibility that is emerging from the Anthologize architecture. Different format translators can deal with data in different ways; to wit:

  • The built-in PDF generator uses XPaths and some basic PHP loopage to format the final document. It’s also got some custom helper methods (eg get_book_title()) that it uses to make the the rendering code a bit easier to use.
  • The built-in ePub generator uses XSL transformations to move from the TEI document to the HTML-esque ePub output.
  • Patrick MJ has been working on a set of theming functions that will allow plugin authors to construct a loop very similar to the WordPress post Loop for the display of their data.
  • Because of the nature of PHP applications, export formats could always bypass Anthologize’s TEI and other API options and head directly for the WP database, using some embedded WP_Query/have_posts() loops.

Once your parser has turned Anthologize TEI data into the format necessary for your chosen format, you’ll need to deliver it to the browser by sending the write headers (here’s how ePub does it as an example).

In my next blog post, I’ll use an example to show how a plugin can register itself with Anthologize to take advantage of all these goodies.

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.