Half-Elf on Tech

Thoughts From a Professional Lesbian

Author: Ipstenu (Mika Epstein)

  • Don’t You Give That Girl a Gun

    Don’t You Give That Girl a Gun

    His WordPress site was hacked.

    He’d reported it as a ‘slow site’ and the techs had done an amazing job helping him clean it up, but when it landed in my lap, I took one look at saw backdoors, permissions issues, and vulnerabilities galore. So I did the reasonable, responsible, fair thing. I reinstalled the files, I cleaned up the plugins, and then I saw his theme was behind a paywall, old, and, worse, no longer supported. So I removed the theme from his website (putting it where he could get it back) and switched him to Twenty Fourteen. Then I explained in a rather long email about how his site was hacked, how I determined it, and what he needed to do to get the theme back (basically download it again from the vendor).

    He was mad.

    He argued that I had broken his site and it no longer looked right. This was true. He complained that my service was deplorable because his site looked wrong. This is debatable. He groused that I had to put the theme back. This was not going to happen.

    old fashioned rifle on a wall

    It’s the service conundrum. If you know something’s wrong, do you leave it alone or do you fix it? When I see people post their passwords in public places, I delete them and use bold and italics to chastise them. When I see people doing dangerous things like editing core, I do the same. I try really hard to educate and warn people, so they can be protected from shooting their own foot off. So when I have a rabid customer telling me I need to let them do it … I don’t.

    My job is really to help people fix their sites, and that tends to mean my job is to debug and educate and provide options. But when someone has an abjectly wrong bit of code, like the bevy of people who had their old themes and plugins break when we upgraded them from PHP 5.2 to 5.4, I will regularly go that extra mile and fix the code. That doesn’t mean I don’t educate them, they usually get a quick lecture about why we upgrade promptly, but when someone’s that far off normal that their code won’t work on PHP 5.3, I assume they just don’t know anything.

    The worst part about it, though, is when they argue. They’ve asked you for help and advice, you provide it, they demand you fix it, and at a certain point… they’re just asking the wrong person. Your webhost is not your consultant. While many times we can and will fix the site, when it gets down to code that isn’t working, we can’t be expected to re-write all the code.

    Sometimes we’re going to be the bearers of bad news. Your theme is hacked. Your plugin is vulnerable. Your code won’t work on this server because of reasons. We’re never making an excuse, but we are trying to explain to you why things happen.

    Now I know I’m a little weird, because I think that everyone should be educated in how their site works. Not that I think they need to learn to code, but to understand what’s going on, in broad terms, means you’ll be able to help us help you fix your site. And with that, I expect people to actually listen to what the support techs say. We won’t always be right, especially not with WordPress which has infinite combinations of plugins and themes (it’s a mathematical impossibility to be able to be familiar with everything) but for the most part, we are all trying to learn to be better and faster at debugging.

    But. What do you do when the person you’re trying to help insists on hurting themselves? Like the person with the hacked theme, maybe you’re lucky and your company has a policy that once you know something is malware, you’re legally not permitted to reinstall it. But what if they decide to use a plugin that has a maybe backdoor, like an older version of TimThumb? How big a deal is that? Is it better or worse than helping someone do something that will absolutely kill their SEO?

    For me, it’s pretty simple. My company does have a no-malware policy, and I can fall back on that. When I volunteer, I often tell people “I will not assist you in doing something I don’t feel is right.” and I walk away. Because I feel strongly that I should educate you, but also that I should never enable you to hurt your site.

  • All Comments By Email

    All Comments By Email

    By default, when you look at the list of comments on your WP Admin dashboard, you get a list like this:

    The Edit Comments page

    On that list, if you click on the IP address of the commenter, you go to all comments by that IP, but if you click on the email you get a mailto link, to let you email the person. That’s great, but as my friend, and fellow fansite runner, Liv pointed out, a lot of people post from multiple IP addresses these days, but only one email. What she wanted was for the icon that gave you the number of approved comments to link to that person’s approved comments.

    Me, being the sort to poke around, decided to see if that could be done. I already knew how to filter columns and tables, after all. What I learned was that there actually isn’t a filter for those columns, and the only way around it was to replace it. This means I was going to have to rebuild everything, and in doing so, I wanted that email address to be a link to the search. While annoying, it was pretty easy:

    <?php
    /*
    	Plugin Name: Easy Comment Search By Email
    	Description: Changes the default link for emails in the comment lists from mailto links to search results for that address.
    	Author: Mika A Epstein (ipstenu)
    	Author URI: https://halfelf.org
    
    Credit: https://wordpress.stackexchange.com/questions/83769/hook-to-edit-an-column-on-comments-screen
     */
    
    class ecsbePlugin {
    
    	public function __construct() {
    		add_action( 'admin_head' , array( &$this, 'column_style') );
    
    	add_filter( 'manage_edit-comments_columns', function($columns) {
    		unset($columns["author"]);
    		$columns_one = array_slice($columns,0,1);
    		$columns_two = array_slice($columns,1);
    		$columns_one["author-new"] = "Author";
    		$columns = $columns_one + $columns_two;
    		return $columns;
    	});
    
    	add_filter( 'manage_comments_custom_column', function($column, $column_id) {
    		
    		global $comment_status;
    				$author_url = get_comment_author_url();
    					if ( 'http://' == $author_url )
    							$author_url = '';
    					$author_url_display = preg_replace( '|http://(www\.)?|i', '', $author_url );
    					if ( strlen( $author_url_display ) > 50 )
    							$author_url_display = substr( $author_url_display, 0, 49 ) . '…';
    	
    					echo "<strong>"; comment_author(); echo '</strong><br />';
    					if ( !empty( $author_url ) )
    							echo "<a title='$author_url' href='$author_url'>$author_url_display</a><br />";
    	
    					if ( current_user_can( 'edit_posts' ) ) {
    						$author_email = get_comment_author_email();
    					
    							if ( !empty( $author_email ) ) {
    									echo '<a href="edit-comments.php?s=';
    									echo $author_email;
    									echo '&mode=detail';
    						   		if ( 'spam' == $comment_status )
    									echo '&comment_status=spam';
    								echo '">';
    								echo $author_email;
    								echo '</a><br />';
    							}
    							
    							echo '<a href="edit-comments.php?s=';
    							comment_author_IP();
    							echo '&mode=detail';
    							if ( 'spam' == $comment_status )
    									echo '&comment_status=spam';
    							echo '">';
    							comment_author_IP();
    							echo '</a>';
    					}
    		
    		}, 10, 2 );
    
    	}
    
     	public function column_style() {
    		echo '<style type="text/css">
    			#comments-form .fixed .column-author-new {
    				width: 20%;
    			}
    		 </style>';
    	}
    
    }
    
    new ecsbePlugin();
    

    The plugin needs a way better name, though, because this is just … bad. The array slice in the beginning was to remove the first item and replace it, without having to do a lot of overly wrought arguing with possible columns.

    That said, this is the sort of thing I may submit a patch for in core, since IPs change a heckuvalot more now, and while that’s a great way to find some serial-accounts and sockpuppets, sorting by email helps you find people being trolls. Both would be good, and I don’t think a lot of us email people. If anything, I’d change the author NAME to be a mailto link.

    Food for thought.

  • How Many Plugins Is Too Many?

    How Many Plugins Is Too Many?

    Have you ever played “Name That Tune”? They used to have this show where they’d play music and you had to name that tune. One of the mini-games in the show was called “Bid-A-Note” where the host read a clue and the contestants would alternate bidding. “I can name that tune in X notes..” where X was a whole number, and the bids ended when one of the contestants challenged the other to “Name That Tune” (or if someone bid one or zero notes).

    Well. I can crash a WordPress site with one plugin.

    When people ask why their site is slow, sometimes my coworkers will say “It’s the plugins, right? He has 40 plugins!” and I’ll say “Maybe.” Then I look at what the plugins are, because it’s never the number of plugins, but their quality. Take a look at Jetpack, which is 33 plugins in one. Is that going to cause more or less overhead than if you had 33 separate plugins installed?

    WordPress is wonderful and beautiful because you can use plugins to do absolutely anything. At the same time, that beauty is it’s downfall, because you can use plugins to do anything. There are over 32,000 active plugins in the WordPress plugin repository. There are probably 4000 or so that are delisted or disabled. There are around 3000 more plugins on just one popular WordPress theme and plugin site. We haven’t even started listing themes.

    It’s a mathematical impossibility to test every possible plugin combination with every theme on every server on every host with every extra (like mod_pagespeed or CloudFlare) added on. It’s impractical to expect every combination to play nicely together, not because of any defectiveness in the code of the plugin or WordPress, but because of the reality that all of those things vary from place to place. We build out things to be flexible, after all.

    I love the flexibility. I think it’s awesome. But at the same time, I worry about it when people complain their site is slow. There’s, very rarely, one perfect answer. Not even “Oh, he was hacked” is the answer to why a site is slow, though it can be. The answer is invariably in the combinations and permutations of what someone has done with their site, what the visitors do with it, and how they interact. A site that posts once a week is different than one that posts four times a day. A site with no comments is different than one with 30 per post. And the more of those little differences you factor in, the harder it gets to determine how many plugins is too much.

    Maybe it’s your memory. One plugin may need more memory than another, but the combination of two may need more than either would individually! Sadly, it’s not something you’re going to know until you start studying your own site. There are cool tools like P3 Profiler which do a great job of giving you an idea as to what’s going on, but it’s not the whole picture. It can’t be. Just look at all the tools we list for performance testing and consider how many and varied the results are.

    How many plugins are too many? However many it takes to kill your site.

    Oh, the one plugin I can run to crash a site? It was BuddyPress and I was using PHP CGI. Once I changed it to a different flavor of PHP, the issue went away.

  • Just Push Publish

    Just Push Publish

    “Real artists ship.” — Steve Jobs, 1983

    I don’t write good all the time. I’m a little lazy and spell poorly, I don’t proofread enough, and if I had a genie to grant me a wish, one would be for an editor who I wasn’t related or married to. When I post a new article, I often see typos and while I do go back and fix them, I still push publish (or schedule), knowing things aren’t perfect.

    This is a major departure from the “traditional” way of writing, when you write, have it reviewed, edit, re-write, re-edit, and so on. To many people, this is seen as ‘lazy’ writing, where we toss out things that are ‘good enough’ and call it a day, but the reality is that publishing promptly, be it writing or code, is what keeps up with the fast changing pace of news, information, and needs. But when it comes to writing, it falls a little bit under the aegis of “If you build it, they will come.” Or rather, if you don’t build it, they won’t come at all.

    Handwriting sample

    Publish or Perish

    Well known to academia is the concept that if you don’t constantly publish works to sustain your career, you won’t have a career. The added pressure is that you have to publish fast so your information isn’t out of date before it hits the ground. The idea is that if you’re not publishing something then you’re not producing something, and you’re thereby sitting on your laurels. In software and blogging, this is actually important too! If you’re not producing code, or writing about it, you’re not demonstrating what you’ve learned. If you release code or write on your blog once a year, people will forget about you.

    Release and Iterate

    Also known as “Release early, release often,” this model of development makes important the concept of early and frequent releases. This necessitates people test, though, and developers respond quickly to issues reported by users. WordPress works by this model. Reid Hoffman, the founder of LinkedIn, said “If you are not embarrassed by the first version of your product, you’ve launched too late.” And if you look at many of the recent technological innovations (including the iPhone), version one was okay, but not great, and had a lot of bugs and annoyances.

    Fear, Uncertainty, Doubt

    The biggest hold up to most of us pushing that publish button is FUD. What if we’re wrong? What if we’re saying things no one cares about? What if… We don’t want to be horribly embarrassed by that typo where we get their/they’re/there wrong, or worse, where we get all that technical information wrong. And it’s that place of fear, that home of uncertainty, that realm of doubt, that we stop. We don’t share what we know, we don’t explain what we think, and we turtle up.

    Democratizing Publishing

    The mission of the WordPress open source project is to democratize publishing through Open Source, GPL, software. By letting any of us write what we want, we’re able to publish at will. That anyone can upload a book to Kindle or Apple and sell their works has changed the world. In many ways, it’s lowered the bar so anyone can sell anything which causes a dearth of quality. And yet, the stamp of quality products has never rested solely in the hands of ‘official’ publishers. Some of the best music we heard was from underground tapes made in basements. Some of the best stories we read were mimeographed in purple ink and handed out on the QT at fan conventions. All we’ve done here is take the barriers away and given you the freedom to say what’s on your mind.

    Write the Change You Want to See

    It takes bravery to post your thoughts, technical or personal, out there. You should only put out work you can stand behind, you should put the best work you can do out there, but you should be willing to post. You should be willing to release that code. You won’t grow, you can’t grow, if you don’t step up and put yourself out there.

    Don’t worry. We know you’ll fix it.

  • Home Affects Your Website

    Home Affects Your Website

    There’s a vulnerability with an old version of MailPoet, which according to Sucuri, is the reason for the breaking of ‘thousands’ of WordPress sites. I do not doubt their claim, nor the validity of the statement, but I did wince mightily at their wording.

    At the time of the post, the root cause of the malware injections was a bit of a mystery. After a frantic 72 hours, we are confirming that the attack vector for these compromises is the MailPoet vulnerability. To be clear, the MailPoet vulnerability is the entry point, it doesn’t mean your website has to have it enabled or that you have it on the website; if it resides on the server, in a neighboring website, it can still affect your website.

    All the hacked sites were either using MailPoet or had it installed on another sites within the same shared account (cross-contamination still matters).

    I bolded the important part here.

    I disagree with the broad, sweeping, implication that statement makes. While they do mitigate that with the next paragraph (and yes, you should read the links), it gives a bad impression as to what the issue really is there. If the vulnerable code resides on your server, under your user account, in a web-accessible directory, then yes, it can affect your website. However for any decent webhost, your site being vulnerable will not result in my domain being hacked.

    Good hosts don’t permit users to access each other’s files. I know it’s semantics, but the implication is that a stranger’s website on your server will make you vulnerable. And that’s just not a given. I know that explaining the nature of relationships between user accounts and access is fraught with complexity, but this is a place where I look at security sites and bang my head on the table because they’re not educating people.

    The way security works for most people is entirely an FUD scenario. They fear what they don’t understand, which generates more uncertainty and doubt. I spent time recently trying to break down that wall and talk about the behaviors in us that make things risky, and I’ll be speaking at WordCamp LA about it in September of this year. I understand totally why Sucuri, and many other people, phrase it this way, but since I firmly believe that education is the only true way to mitigate hacked sites, I want to explain the relationship of files to people.

    A bed in a jail cell

    If you’ve ever FTPd or SSHd into your website, you know you have a user ID. That ID owns the files on your server, but it’s not the only account on a server. Your ID is yours and yours alone. You can give someone else the password, but please don’t unless you trust them with your car. Once you’re logged in with your account, everything you see is connected. This means if you can see it, then anyone else who gets into your account can see it.

    How does WordPress play into this? Well if you can see it logged in, then so can WordPress, to an extent. If a plugin or a theme has a specific kind of vulnerability, then it can be used to extract information like everything under that user account. A pretty common vulnerability I see is where the plugin allows you to read any file on the system, including the wp-config.php file, which gives people your database username and password (and it’s why I tell people to change all their passwords).

    A very common thing for people to do, and I do this myself, is to run multiple domains under one user account. Many times they’re called ‘add on’ domains. In this case, you can actually visit https://ipstenu.org/helf.us/ and see the same site as you would at https://helf.us. This is problem fairly easily fixed with .htaccess (though if, like me, you also have mapped domains, it gets much messier):

    RewriteEngine On
    RewriteCond %{HTTP_HOST} ^(www.)?example.com$ [NC]
    RewriteCond %{REQUEST_URI} ^/addon1/(.*)$ [OR]
    RewriteCond %{REQUEST_URI} ^/addonN/(.*)$
    RewriteRule ^(.*)$ - [L,R=404]
    

    All that said, if someone knows that helf.us and ipstenu.org are on the same server, and the software I use on one is vulnerable, it can be shockingly trivial to infect the other.

    What is not trivial would be using an exploit on ipstenu.org to hack ipstenu.com. Yes, it redirects you to ipstenu.org, but it is a real website. The reason I would be shocked to find it infected, if ipstenu.org was, is that they’re under separate user accounts. If you logged in with the ipstenuorg ID, you would not, could not, see ipstenucom.

    ipstenuorg@ipstenu.org [/home]# ls -lah
    /bin/ls: cannot open directory .: Permission denied
    

    And even if they knew there was a folder called ipstenucom, the couldn’t do anything about it except get in:

    ipstenuorg@ipstenu.org [/home]# cd ipstenu.com
    ipstenuorg@ipstenu.org [/home/ipstenu.com]# ls -lah
    /bin/ls: cannot open directory .: Permission denied
    ipstenuorg@ipstenu.org [/home/ipstenu.com]# cd public_html
    -bash: cd: public_html: Permission denied
    

    The separation of the users is going to protect me.

    So to reiterate, if a site (or the account that owns a site) has access to other sites, and is hacked, yes, those other sites are at high risk. If the site has no access to anything but itself, they will not be hacked. And as I said before, most hosts go to tremendous lengths to ensure you cannot read someone else’s files or folders. The whole reason I can get into the ipstenucom is that the permissions on that folder allow it. Would it be safer to prevent it? Sure! And actually that’s not what you normally see when you’re on my servers.

    ipstenuorg@ipstenu.org [~]# cd ../
    ipstenuorg@ipstenu.org [/home]# ls -lah
    total 12K
    drwx--x--x 37 ipstenuorg ipstenuorg 4.0K Jul 23 02:04 ipstenu.org/
    ipstenuorg@ipstenu.org [/home]# cd ipstenu.com
    -jailshell: cd: ipstenu.com: No such file or directory
    

    That’s right, I use jailed shell to prevent shenanigans, and even when I don’t, things are remarkably safe because I don’t permit users to snoop on other users. That said, as I was reminded we must never underestimate the ability of a fool, playing at sys admin work, to take their own pants down. It’s possible for a user to set up their own domain to be searchable by other accounts on the server, and to make it writable to those other users, which can cause a lot of problems.

    Here’s your takeaway. Everything that is installed on your domain, active or not, is a potential vulnerability. Upgrade everything as soon as you can, delete anything you’re not using, don’t give more people the keys to the castle than you have to, and try really, really hard to think about what you’re doing.

  • Mailbag: Pinging Pingbacks

    Mailbag: Pinging Pingbacks

    I run a fan site, and so does a friend of mine. Liv and I were chatting about wishlists in WordPress for fansites, and she mentioned this:

    I also like seeing who has linked to my site from other WP blogs because that helps me create fandom connections with other bloggers. I wish there was a quick button I could hit that would allow me to email those bloggers with a quick note of thanks for the connection

    When you’re running a fan website, communicating and connecting with those other sites is a killer feature. We network and that’s how we make our communities bloom, after all, since most of us can’t afford a budget for ‘real’ advertising, and it’s probably not entirely legal for us to do that anyway. So outside of spending days tracking everyone down, what about using the power of ping-backs for ourselves?

    Table Tennis

    I’m sure Liv has an unshakable confidence in my ability to code her things (and I love the requests she makes, they stretch my brain) but this one kicked my patootie a lot. Getting a list of pingbacks isn’t all that hard. There’s a plugin called Commenter Emails by Scott, which nicely lists all the email addresses used to make comments. Using that logic, it’s pretty easy to list all the pingbacks. I mean, hey, we can already do that!

    If you go to /wp-admin/edit-comments.php?s&comment_status=all&comment_type=pings you’ll see all your pings:

    All pings listed

    Just looking at that, however, made me notice a horrible problem. There are no emails listed in pingbacks. This makes perfect sense. The emails aren’t (generally) listed on a page that links to your site. That means without doing some serious site-scraping, there’s no way to get that email.

    Putting that aside, the other option is to, perhaps, list the ‘parent’ domain that pinged you. So I went back to Scott’s plugin and forked it into this:

    <?php
    
    /*
    Plugin Name: Pingers List
    License: GPLv2 or later
    License URI: http://www.gnu.org/licenses/gpl-2.0.html
    Description: List all pingbacks with links to their main domain.
    
    	Quasi fork of http://wordpress.org/plugins/commenter-pings/
    
    	Copyright (c) 2007-2014 by Scott Reilly (aka coffee2code)
    	Copyright (c) 2014 by Mika Epstein (aka ipstenu)
    */
    
    defined( 'ABSPATH' ) or die();
    
    if ( is_admin() && ! class_exists( 'PingersList' ) ) :
    
    class PingersList {
    
    	private static $plugin_basename = '';
    	private static $plugin_page     = '';
    
    	/**
    	 * Returns version of the plugin.
    	 *
    	 * @since 2.1
    	 */
    	public static function version() {
    		return '2.2.1';
    	}
    
    	/**
    	 * Constructor
    	 */
    	public static function init() {
    		self::$plugin_basename = plugin_basename( __FILE__ );
    
    		// Register hooks
    		add_action( 'admin_menu', array( __CLASS__, 'admin_menu' ) );
    		add_action( 'admin_menu', array( __CLASS__, 'do_init' ), 11 );
    	}
    
    	/**
    	 * Initialize hooks and data
    	 */
    	public static function do_init() {
    		// Currently empty
    	}
    
    	/**
    	 * Query database to obtain the list of commenter email addresses.
    	 * Only checks comments that are approved, have a author email, and are
    	 * of the comment_type 'comment' (or '').
    	 *
    	 * Only one entry is returned per email address.  If a given email address
    	 * has multiple instances in the database, each with different names, then
    	 * the most recent comment will be used to obtain any additional field data
    	 * such as comment_author, etc.
    	 *
    	 * @param array $fields  The fields to obtain from each comment
    	 * @param string $output (optional) Any of ARRAY_A | ARRAY_N | OBJECT | OBJECT_K constants. See WP docs for wpdb::get_results() for more info
    	 * @return mixed List of email addresses
    	 */
    	public static function get_pings( $fields = array(  'comment_post_ID', 'comment_author', 'comment_author_url' ), $output = ARRAY_N ) {
    		global $wpdb;
    
    		// comment_author_url must be one of the fields
    		if ( ! in_array( 'comment_author_url', $fields ) )
    			array_unshift( $fields,  'comment_author_url' );
    
    		$fields = implode( ', ', $fields );
    		$sql = "SELECT $fields
    				FROM {$wpdb->comments} t1
    				INNER JOIN ( SELECT MAX(comment_ID) AS id FROM {$wpdb->comments} GROUP BY comment_author_url ) t2 ON t1.comment_ID = t2.id
    				WHERE
    					comment_approved = '1' AND
    					comment_type = 'pingback'
    				GROUP BY comment_author_url
    				ORDER BY comment_author_url ASC";
    		$pings = $wpdb->get_results( $sql, $output );
    		return $pings;
    	}
    
    
    	/**
    	 * Creates the admin menu.
    	 *
    	 * @return void
    	 */
    	public static function admin_menu() {
    		add_filter( 'plugin_action_links_' . self::$plugin_basename, array( __CLASS__, 'plugin_action_links' ) );
    		// Add menu under Comments
    		self::$plugin_page = add_comments_page( __( 'Pinger List', 'pinger-list' ), __( 'Pinger List', 'pinger-list' ),
    			apply_filters( 'manage_commenter_pings_options', 'manage_options' ), self::$plugin_basename, array( __CLASS__, 'admin_page' ) );
    	}
    
    	/**
    	 * Adds a 'Settings' link to the plugin action links.
    	 *
    	 * @param array $action_links The current action links
    	 * @return array The action links
    	 */
    	public static function plugin_action_links( $action_links ) {
    		$settings_link = '<a href="edit-comments.php?page=' . self::$plugin_basename.'" title="">' . __( 'Listing', 'pinger-list' ) . '</a>';
    		array_unshift( $action_links, $settings_link );
    		return $action_links;
    	}
    
    	/**
    	 * Outputs the contents of the plugin's admin page.
    	 *
    	 * @return void
    	 */
    	public static function admin_page() {
    		$pings = self::get_pings();
    		$pings_count = count( $pings );
    
    		echo '<div class="wrap">';
    		echo '<h2>' . __( 'Ping List', 'pinger-list' ) . '</h2>';
    		echo '<p>' . sprintf( __( 'There are %s unique ping locations for this site.', 'pinger-list' ), $pings_count ) . '</p>';
    		echo '</div>';
    
    			echo '<div class="wrap">';
    			echo '<h2>' . __( 'All Pings', 'pinger-list' ) . '</h2>';
    			echo '<table padding=2>';
    			echo '<tr><th>' . __( 'Post', 'pinger-list' ) . '</th><th>' . __( 'Source', 'pinger-list' ) . '</th><th>' . __( 'Direct Link', 'pinger-list' ) . '</th></tr>';
    
    			foreach ( $pings as $item ) {
    			
    				$pings_url = parse_url(esc_html( $item[2] ));
    				$ping_url = $pings_url[scheme].'://'. $pings_url[host];
    			
    				echo '<tr width="20%"><td><a href="' . get_permalink( $item[0] ) . '">'. get_the_title($item[0]) .'</a></td>';
    				echo '<td width="20%">' . make_clickable($ping_url).'</td>';
    				echo '<td><a href="'.esc_html( $item[2] ).'">'. esc_html( $item[1] ) . '</a></td></tr>';
    			}
    
    			echo '</table>';
    			echo '<p>' . sprintf( __( '%s pings listed.', 'pinger-list' ), $pings_count ) . '</p>';
    			echo '</div>';
    
    
    	}
    } // end PingersList
    
    PingersList::init();
    
    endif; // end if ! class_exists()
    

    The plugin’s crazy basic. It simply checks for unique ping sources and lists them. So if the same ‘main’ site links to you 10 times from 10 separate posts, it lists that. Probably a nice tweak would be to order them by domain, list the posts they link to and from where, and have a group by sort of list, but I didn’t get that far into it. Forks welcome, as are full blown plugins!