Category Archives: Web Development

Fixing WordPress Paragraph Spacing (When the Visual Editor Misleads You)

A client of mine who blogs with WordPress ran into an issue with new posts appearing on her site without the proper spacing between paragraphs. I’m sharing the solution in case others might find it useful.

Everything looked fine in the WordPress visual editor, but these posts looked wrong on the public website. I thought at first it was problem with the site theme / CSS, but older posts on the same site were displaying fine. The only way to figure out what was going wrong was to view these posts with the editor toggled from Visual to Text to reveal some code that shouldn’t have been there. Every paragraph was tagged as a div — which in HTML is a block of text with no default styling. Also, an empty div is not displayed at all, so the blank lines she thought she had in her copy disappeared on the public site.

I suspect the divs got in there because this content had been copied and pasted from some other source, probably some other web-based word processor or blog posting system. WordPress usually does a pretty good job of handling content posted in from Microsoft Word, but not necessarily other sources.

Post with excess divs

Post with excess divs

In visual mode, WordPress identified all these divs as paragraphs, by the way, which was not helpful. Once you have paragraphs tagged as divs, WordPress keeps tagging any additional content you insert the same way.

To fix the posts, I had to manually remove the <div> and </div> tags. What a WordPress post normally looks like in the editor is a mix of text and HTML (for links and images) with a blank line between paragraphs. WordPress normally handles these formatting chores automatically — it just doesn’t handle an attack of the divs very well.

Properly formatted post in the WordPress editor.

Properly formatted post in the WordPress editor.

Notifier for Glip, a new WordPress plugin

This plugin will post a notification to a Glip team conversation whenever a new blog article is posted or a comment is posted to a blog that you maintain. The plugin uses the WebHooks interface to connect with Glip, the team collaboration and productivity platform from RingCentral.

The plugin is available through the WordPress.org plugins repository: https://wordpress.org/plugins/notifier-for-glip/

I do editorial consulting for Glip, so this came about because I was trying to cook up an example of using their support for WebHooks (a standard web services API). Commented code included below.

Target audience for this plugin:

  • Anyone who runs a WordPress website with multiple contributors and people posting comments.
  • Web developers and designers who need to keep tabs on a client’s blog posts and comment activity.
WordPress new blog post and comment activity, as recorded in the stream of comments in a Glip team conversation.

WordPress new blog post and comment activity, as recorded in the stream of comments in a Glip team conversation.

How It Works

The WordPress admin must set the WebHooks url for Glip on the Settings -> Glip screen.

Here is where you obtain that information from within Glip, after activating the WebHooks integration.

Glip WebHooks integration screen

Glip WebHooks integration screen

The plugin uses WordPress action hooks to detect new posts and comments and relay them to Glip in the JSON format specified above.

Here is the code I used for the initial release:

<?php
/*
Plugin Name: Notifier for Glip
Plugin URI: http://www.carrcommunications.com/notifier-for-glip/
Description: Post a notification to a Glip team conversation whenever a new blog article is posted or a comment is posted to a blog that you maintain. The plugin uses the WebHooks interface to connect with Glip, the team collaboration and productivity platform from RingCentral.
Author: David F. Carr
Version: 0.9
Author URI: http://www.carrcommunications.com
*/

function glip_webhook($webhook_url,$title, $body, $activity,$icon ='' ) {

$json = json_encode(array('icon'=>$icon,'activity'=>$activity,'title'=>$title,'body'=>$body) );

$ch = curl_init($webhook_url);                                                                     
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");                                                                     
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);                                                                  
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);                                                                      
curl_setopt($ch, CURLOPT_HTTPHEADER, array(                                                                          
    'Content-Type: application/json',                                                                                
    'Content-Length: ' . strlen($json))                                                          
);                                                                                  
$r = curl_exec($ch);
return $r;
}

function glip_post_published_notification( $ID, $post, $update ) {
	
	if ($post->post_date != $post->post_modified)
		return; // don't do this for edits to a previously published post
	$webhook_url = get_option('glip_webhook'); 
	if(empty($webhook_url))
		return; // won't work without it
	
    $author = $post->post_author; /* Post author ID. */
    $name = get_the_author_meta( 'display_name', $author );
    $email = get_the_author_meta( 'user_email', $author );
    $title = $post->post_title;
    $permalink = get_permalink( $ID );
    $edit = get_edit_post_link( $ID, '' );
	$start = substr(strip_tags($post->post_content),0,100);
	$title = $update.sprintf('New post to blog: [%s](%s) by %s %s',$title, $permalink,$name,$email);
	$body = sprintf('%s

%s

[Edit](%s)',$_SERVER['SERVER_NAME'],$start,$edit);
	$icon = plugins_url('wordpress-logo-32-blue.png',__FILE__);
	$activity = 'New Blog Post';
	glip_webhook($webhook_url,$title, $body, $activity,$icon);
}
add_action( 'publish_post', 'glip_post_published_notification', 10, 2 );

add_action('wp_insert_comment','glip_comment_inserted',99,2);

function glip_comment_inserted($comment_id, $comment_object) {

	$webhook_url = get_option('glip_webhook'); 
	if(empty($webhook_url))
		return; // won't work without it

	$permalink = get_permalink($comment_object->comment_post_ID);
	$title = sprintf('New comment on blog by %s %s',$comment_object->comment_author, $comment_object->comment_author_email);
	$body = sprintf('%s
	
	%s
	
	Post: [%s](%s)',$_SERVER['SERVER_NAME'],$comment_object->comment_content,get_the_title($comment_object->comment_post_ID),$permalink);
	$icon = plugins_url('wordpress-logo-32-blue.png',__FILE__);
	$activity = 'New Comment on Blog';
	glip_webhook($webhook_url,$title, $body, $activity,$icon);
}

add_action('admin_init', 'glip_options_init' );
add_action('admin_menu', 'glip_options_add_page');

// Init plugin options to white list our options
function glip_options_init(){
	register_setting( 'glip_webhook_options', 'glip_webhook', 'glip_options_validate' );
}

// Add menu page
function glip_options_add_page() {
	add_options_page('Glip', 'Glip', 'manage_options', 'glip_options', 'glip_options_do_page');
}

// Draw the menu page itself
function glip_options_do_page() {
	?>
<div class="wrap">
		<h2>Glip Options</h2>
		<form method="post" action="options.php">
			<?php settings_fields('glip_webhook_options'); ?>
			<?php $webhook = get_option('glip_webhook'); ?>
       WebHooks address:     <input name="glip_webhook" id="glip_webhook"value="<?php echo $webhook?>" />
			<p class="submit">
			<input type="submit" class="button-primary" value="<?php _e('Save Changes') ?>" />
			</p>
		</form>
		<p>You will find this web address in the settings screen for the WebHooks integration, which is included with every Glip account. A menu in the upper right hand corner allows you to change the team conversation updates will be posted to.</p>

<p><img src="<?php echo plugins_url('glip-webhooks.png',__FILE__); ?>" width="580" height="372" alt="Webhooks" /></p>

	</div>
	<?php	
}

// Sanitize and validate input.
function glip_options_validate($input) {

	if (!filter_var($input, FILTER_VALIDATE_URL) === false) {
		return $input;
	} else {
		return '';
	}

}

function glip_admin_notice () {
$w = get_option('glip_webhook');
if(empty ($w) )
	printf('<div class="error">%s <a href="%s">%s</a></div>',__('WebHooks URL must be set for ','glipnotifier'),admin_url('options-general.php?page=glip_options'), __('Glip integration','glipnotifier'));
	
}

add_action('admin_notices', 'glip_admin_notice');
?>

My First Redbooth API Demo

Shortly after joining Redbooth as an employee, I began playing with the collaboration platform’s application user interface. As a writer who also writes code, I was curious to see whether I could come up with something useful.

The Redbooth API makes use of the OAuth2 authentication method, which provides a standard way for a user of one application to give permission for another application to access protected resources. For example, once the test is past, an external application that has received the user’s blessing can retrieve access files stored in a Redbooth workspace or add tasks to a task list. The application retrieves a cryptographic authentication token from Redbooth, which it then uses as proof of its right to access these resources.

One of the first ideas I had was to display some sort of interactive user directory. I can imagine a few different scenarios where it might be handy to store some supplemental information about employee performance or vacation schedules in your own app, using the data structure you define rather than one provided within Redbooth.

This version shown below (which you can access live here) pulls the names and profile photos for all the other users in your organization and allows you to add notes to each of these mini-profiles.

Note that you’re welcome to use this application for real, as long as you’re willing to do so at your own risk; it’s not running on Redbooth’s secure infrastructure, and the data you add goes into a MySQL database on my server. If you have a serious application for something like this, you could use the code shown below as a starting point (if you’re working in PHP) or as inspiration for an app you might implement in the language of your choice, on your own server.

API demo

Once a redbooth user authorizes account access, this app displays a basic user directory with the option of adding notes to each user record.

The preliminary setup starts in initialize.php, which sends a request to redbooth.com to start the OAuth authentication process:

initialize.php


<?php

$apikey = 'KEY_GOES_HERE';
 
// make sure to url encode the redirect URL
$q = urlencode('http://tabmgr.com/rb/auth.php'); //provide the url for auth.php on your server
 
// construct the authorization query with our apikey and the redirect URL
$endpoint = 'https://redbooth.com/oauth2/authorize?client_id=' . $apikey . '&redirect_uri=' . $q . '&response_type=code';
 
//redirect to authorization URL
header("Location: " . $endpoint);

exit;
 
?>

The main workhorse functions are in auth.php. In the previous step, we queried redbooth.com for a code to be used to kick off the authentication process. The code is returned as a GET query parameter appended to the redirect uri specified in initialize.php. The API key and API secret are used to verify that the user has given permission for access to his or her redbooth.com account. The remainder of the code in this script is devoted to sorting the user records by last name, formatting them for display, and setting up a JQuery/AJAX routine to post notes about each user to a local database.

Any existing notes are retrieved as part of a database routine that runs right before we start iterating through the user records retrieved from redbooth.com.

auth.php

<?php
 
session_start();

?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>User Directory Demo</title>
<style>
textarea {
width: 600px;
border: thin solid blue;
height: 2em;
}
body {
background-color: #CC0000;
}
#content {
width: 700px;
padding-left: 15px;
padding-right: 15px;
margin-left: auto;
margin-right: auto;
background-color: #eee;
}
</style>
</head>

<body>
<div id="content">
<h1>Dave's unofficial API demo</h1>
<p>Once authorized, the app pulls a list of the other users in your organization from Redbooth. You can add a note to any user record by typing it into the blank and pressing enter.</p>
<?php

function rb_authorize () {

$code = $_GET["code"];
 
$apikey = 'KEY_GOES_HERE';
 
$appsecret = 'SECRET_GOES_HERE';

// make sure to url encode the return URL
$q = urlencode('http://tabmgr.com/rb/auth.php');  //ex: http://www.mytest.com/auth.php

// construct the authorization query with our apikey and the returned code to get the access_token
$endpoint = 'https://redbooth.com/oauth2/token?client_id=' . $apikey . '&client_secret=' . $appsecret . '&code=' . $code . '&grant_type=authorization_code&redirect_uri=' . $q;
 
// setup curl to make a call to the endpoint
$session = curl_init($endpoint);

// indicates that we want the response back
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
// post
curl_setopt($session, CURLOPT_POST, true);
 
// exec curl and get the data back
$data = curl_exec($session);

// remember to close the curl session once we are finished retrieveing the data
curl_close($session);
 
// decode the json data to make it easier to parse the php
$auth_result = json_decode($data);
 
// check for empty data
if ($auth_result === NULL) die('Error parsing json for auth result');

// Get the access_token
$_SESSION["access_token"] = $access_token = $auth_result->access_token;
return $access_token;
}

function get_me($access_token) {
// construct the query with the access_token to get the user's account details
$endpoint = 'https://redbooth.com/api/3/me?access_token=' . $access_token;

// setup curl to make a call to the endpoint
$session = curl_init($endpoint);
 
// indicates that we want the response back
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
 
// exec curl and get the data back
$data = curl_exec($session);
 
// remember to close the curl session once we are finished retrieveing the data
curl_close($session);
 
// decode the json data to make it easier to parse the php
$me = json_decode($data);
return $me;
}

if($_SESSION["access_token"] && !$_GET["new"])
	$access_token = $_SESSION["access_token"]; // todo - check expiration
else
{
// Get the returned code
$access_token = rb_authorize ();
}

$me = get_me($access_token);

// check for empty data
if ($me === NULL)
{
// try again to get current user data
$access_token = rb_authorize ();
$me = get_me($access_token);
}

if ($me === NULL) die('Error parsing json for /me');

// construct the query with the access_token to get the user's account details
$endpoint = 'https://redbooth.com/api/3/users?access_token=' . $access_token;
 
// setup curl to make a call to the endpoint
$session = curl_init($endpoint);
 
// indicates that we want the response back
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
 
// exec curl and get the data back
$data = curl_exec($session);

// remember to close the curl session once we are finished retrieveing the data
curl_close($session);

// decode the json data to make it easier to parse the php
$users = json_decode($data);
 
// check for empty data
if ($users === NULL) die('Error parsing json for users listing: '.$data);

$count = sizeof($users);

echo "<p><em>Fetched $count users</em></p>";

// get any status notes about the users from the database and put them in an array
// keyed to the redbooth username
include "db.php";
$sql = "SELECT * FROM status ORDER BY timestamp DESC";
foreach ($conn->query($sql) as $row) {
	$stat[$row['username']] .= '<p>'.$row['text'] . "<br /><em>posted by @" . $row["by"] .' at ' . date('F j, Y',strtotime($row["timestamp"]))."</em><p>\n";
}

foreach($users as $user)
	{
	$uindex = strtolower(preg_replace('/[^A-Za-z]/','',$user->last_name.$user->first_name.$user->email));
$user_directory[$uindex] = sprintf("
<div style=\"float: right; clear:both\"><img src=\"%s\"></div>
<p>
<strong>First Name</strong>: %s<br />
<strong>Last Name</strong>: %s<br />
<strong>Email</strong>: %s
</p>
<p>
<textarea class=\"status\" id=\"get_%s\" subject_username=\"%s\" me_username=\"%s\"></textarea>
</p>
<div id=\"postresult_%s\" ></div>
%s
",$user->avatar_url, $user->first_name, $user->last_name, $user->email, $user->username, $user->username, $me->username, $user->username, $stat[$user->username]);
	}

ksort($user_directory);
foreach ($user_directory as $profile) 
	echo $profile;
?>
</div>
<script src="https://code.jquery.com/jquery-1.10.2.js"></script>
<script>
$( ".status" )
  .keypress(function() {
     if(event.keyCode == 13)
       {
	PostRbStatus(this.getAttribute("subject_username"), this.getAttribute("me_username"), this.value)
       }
  })  

function PostRbStatus(subject_username, me_username, message) {
	
$.post( "/rb/poststatus.php", {subject_username: subject_username, me_username: me_username, message: message})
	.done(function( data ) {
  $( "#postresult_" +  subject_username).html( data )
	})
	.error(function( ) {
		alert("error");

});

	$('#get_' + subject_username).blur();
	$('#get_' + subject_username).value('');
	
}

$( "textarea" )
  .on( "mouseenter", function() {
    $( this ).css({
      "border": "medium solid #CC0000",
	  "height": "5em"
    });
  })
  .on( "mouseleave", function() {
    var styles = {
      "border": "thin solid blue",
	  "height": "2em"
    };
    $( this ).css( styles );
});
</script>
</body>
</html>

The db.php file is a standard database initialization using the PDO class.

<?php
$servername = "localhost";
$username = "USERNAME_GOES_HERE";
$password = "PASSWORD_GOES_HERE";

try {
    $conn = new PDO("mysql:host=$servername;dbname=tabmgr_rb", $username, $password);
    // set the PDO error mode to exception
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    //echo "Connected successfully"; 
    }
catch(PDOException $e)
    {
    echo "Database connection failed: " . $e->getMessage();
    }
?>

The database access a simple table with this structure.


CREATE TABLE IF NOT EXISTS `status` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `by` varchar(255) NOT NULL,
  `text` text NOT NULL,
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

Here is the server-side script that processes the data submitted for notes on each user. The output is captured by the client-side JavaScript and placed in a div above the previous entries in the database.

<?php
include "db.php";
if($_POST["subject_username"])
	{
		$username = $_POST["subject_username"];
		$by = $_POST["me_username"];
		$message = $_POST["message"];
		$sql = sprintf("INSERT INTO status (`username`, `by`, `text`) VALUES('%s', '%s', '%s') ", $username, $by, $message);
		$count = $conn->exec($sql);
		if($count)
			printf('<p>%s <br /><em>added by %s %s</em></p>',$message,$by,date('F j, Y'));
		else
			echo "<p>Error adding note</p>";
	}
?>

WordPress for Toastmasters Project

One of my recent web development projects is a customization of WordPress for use by Toastmasters clubs, available as software as a service at wp4toastmasters.com.

This is an extension of RSVPMaker, my most successful and widely used WordPress plugin. I added Toastmasters-specific features while serving as Vice President of Education (and later President) at Club Awesome Toastmasters. Now I’m taking what I developed for my own selfish purposes and making it more widely available.

The Toastmasters extensions may or may not see the light of day as a published plugin, but I’m offering a free version of the hosted software to those who want a subdomain such as myclub.wp4toastmasters.com – with the possibility of upgrades for those who want to host at their own domain.

You can see a bit of how it works in the video below.

Social Collaboration for Dummies book launch at JiveWorld

JiveWorld Moderation

Doing onstage interviews at JiveWorld; here with Lisa Araonson of Allstate

The release of my Social Collaboration For Dummies book turned out to be just in time for JiveWorld13, the Jive Software user conference I participated in. I do mean just in time — the books turned up in the warehouse at Wiley the Friday before the event and had to be overnighted to Los Vegas to catch up with me.

In addition to a holding a book signing at the show, I participated on an analysts panel and conducted a series of onstage interviews with JiveWorld customers Allstate, American Airlines, and Steelcase.

One of the neat things about the Allstate story is they tried instituting social collaboration a few years ago but very tentatively, deciding at that time that employees associated with crucial operations such as claims shouldn’t have access to the enterprise social network because it might be a distraction to them. Today, the organization has done a 180 — figuring out that the claims group needs this technology precisely because it is so critical. In part, that’s because social collaboration could fill a knowledge transfer role, giving younger workers a medium for getting questions answered by more experienced workers, with the record of those Q&A interactions preserved online. With the addition of a mobile client, the social network has also become more useful to personnel in the field, who often need to look up a document or get a question answered while on the go.

Allstate and Steelcase both had a combination of stories about internal social networking and the creation of collaboration groups for external constituencies (independent insurance agents and furniture dealers, respectively).

American Airlines was using the social collaboration network partly to demonstrate a new spirit of transparency and cooperation to a workforce badgered by years of turmoil. At the same time, it is moving cautiously in how it opens up that resource because of workforce and union tensions. Another common large company use for social collaboration is to bridge the gaps between business units brought together through mergers and acquisitions, and American has some plans to use the network to help it integrate United Airlines — assuming that one survives a current challenge by the justice department.