Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: plugins

  • More Complicated Composition

    More Complicated Composition

    Once you’ve made your basic composer.json file and you’ve got your library included, it’s time to consider a more complication situation.

    Let’s take the following example. You want to include two plugins/add-ons, a javascript library, oh and one of those plugins doesn’t use Composer. Don’t worry, we can do this.

    Organize Your Notes

    Step one is to make a list of what you need to install and where it needs to go. Because in this situation, due to the use of the javascript, we’re not going to be using that autoloader.

    1. list all your repositories 
    2. mark which ones do and don’t use Composer
    3. determine where they need to go

    For this example, we want the javascript files to go in the assets/js/ folder in our plugin, and we want the plugin/add-ons to go in plugins/

    Okay, that’s straightforward. But we need to address something.

    Avoid AutoTune

    I mentioned we’re not using the autoloader and I cited the javascript as why. That was a partial lie. You see, even though Composer makes an autoload file, and even though it is a dependancy manager, it actually tells you not to commit your dependancies to your own repository.

    The general recommendation is no. The vendor directory (or wherever your dependencies are installed) should be added to .gitignore/svn:ignore/etc.

    Official Composer Documentation

    The biggest reason why is one you absolutely will run into here, and it’s that when you add dependancies installed via git to another git repo, they end up as submodules, which are a special hell of their own. Only it’s worse. They’re not even really submodules, and you end up with empty folders.

    Seriously, I wasted two hours on this when I first ran into it.

    But that said, it’s really wise to omit your vendor folder because your plugin does not need 3 megs of files if all you want is one javascript file. Right? This means we need to add one more dependancy to Composer, and that’s Composer Copy File.

    Adding The Normal Dependancies

    Right now, our requires section looks like this:

        "require": {
            "slowprog/composer-copy-file": "^0.2.1"
        }

    So when you add in your normal dependancies, like the PHP library, you get this:

        "require": {
            "slowprog/composer-copy-file": "^0.2.1",
            "example/php-library1": "^1.3"
        }

    You’ll do this for the javascript libraries as well:

        "require": {
            "slowprog/composer-copy-file": "^0.2.1",
            "example/php-library1": "^1.3",
            "example/js-library1": "^0.2.1",
            "example/js-library2": "^2.30"
        }

    Adding the Weird Stuff

    Okay but I mentioned one of the PHP libraries I wanted to use didn’t have a composer.json file, which means I can’t include it like that. Instead, I have to add this section above the requires section, in order to create a new package to add:

        "repositories": [
            {
              "type": "package",
              "package": {
                "name": "example2/php-library",
                "version": "1.0.0",
                "source": {
                  "url": "https://github.com/example2/php-library",
                  "type": "git",
                  "reference": "master"
                }
              }
            }
        ],

    Right? That’s all kinds of weird, but basically I’m telling Composer that there’s a package called example2/php-library and it gets its data from https://github.com/example2/php-library – which means I can then add it to my requires like this:

        "require": {
            "slowprog/composer-copy-file": "^0.2.1",
            "example/php-library1": "^1.3",
            "example/js-library1": "^0.2.1",
            "example/js-library2": "^2.30",
            "example2/php-library2": "dev-master",
        }

    Copy Everything

    Once you have your files in and required, everything gets put in vendor which is great except for the part where we’re not going to use the autoloader. In fact, we’re going to add /vendor/ to our .gitignore file to make sure we keep our plugin small.

    No, we’re going to use that copy code we mentioned above like this:

        "scripts": {
            "post-install-cmd": [
                "SlowProg\\CopyFile\\ScriptHandler::copy"
            ],
            "post-update-cmd": [
                "SlowProg\\CopyFile\\ScriptHandler::copy"
            ]
        },
        "extra" : {
            "copy-file": {
                "vendor/example/php-library1/": "plugins/php-library1/",
                "vendor/example2/php-library2/": "plugins/php-library2/",
                "vendor/example/js-library1/dist/js/file.min.js":  "assets/js/file.min.js",
                "vendor/example/js-library2/dist/js/file2.min.js": "assets/js/file2.min.js"
            }
        }

    The first portion triggers a copy every time you install or update the Composer setup, and the section section (in extras) is what runs. 

    Now you include the libraries in your code like you’d downloaded and copied them, only you don’t have to worry so much about keeping them up to date. As long as you’ve got your composer versions, you’re good to go.

  • Small Steps with Composer

    Small Steps with Composer

    Like a great many people before me, I use composer to manage packages. In my case, I have a WordPress plugin that contains some fairly significant packages that other people have written. They’re libraries, and I have a bit of a love/hate relationship with them. Mostly, I hate keeping them up to date, which is where composer comes in for me.

    Composer?

    Composer bills itself as a package manager for PHP. This means it will download all the code you need for your plugins, toss it in a folder (usually called vendor) and let you get busy with the coding and not worrying about if your PHP library is out of date.

    It’s very similar to bower, which I’ve been using for a while now, and grunt, which I sometimes use with bower. However unlike those, Composer hooks in to Packagist, which allows you to include pretty much any library with a composer.json file to generate your builds. And this is where it gets hairy.

    Conceptualizing Composer

    The basics are these: You need a composer.json file to tell Composer what to do, and in that file you need to tell Composer what to do. Yep, that’s it. The complications come in with understanding exactly what it is you’re trying to do. So let’s start small.

    Today, you want to use Composer to update a library, say the AWS SDK for PHP, so you can include it in your plugin. That’s it. Like I said, small. We’re going to assume you’ve written everything else, and you went to the AWS SDK library on Github to get the files.

    The old way would be to download the zip, unzip it, and include it in your PHP code. The new way is is to make a Composer file.

    Constructing Composer

    Windows users, you need to download the setup file from getComposer.org. Mac/Linux users, I recommend you use the global install method, or if you’re lazy like me, brew install composer will get you done.

    Now that you have it installed, you need to make your file. There are a lot of options and possible calls to put in the file. All you need is the ‘requires’ section, which literally is going to tell Composer what it requires. I recommend a basic file that lists what it’s for, who wrote it, and what it needs. 

    Example:

    {
        "name": "example/my-project-name",
        "description": "This is a very cool package that uses AWS",
        "version": "1.0.0",
        "type": "wordpress plugin",
        "keywords": ["wordpress", "plugin", "self-hosted"],
        "homepage": "https://example.com",
        "license": "MIT",
        "authors": [
            {
                "name": "Test Example",
                "email": "test@example.com"
            },
        ],
        "require": {
            "aws/aws-sdk-php": "3.*"
        }
    }

    The secret sauce is that teeny requires section at the end, where I say what I want to require and what version. That’s how composer update knows what I need.

    You can also make the file without that requires section and then tell Composer to include it via command line: composer require aws/aws-sdk-php — That will write the line for you.

    Calling Composer

    So once you have that and install it and run Composer, how do you get it in WordPress? By default, Composer makes an autoloader file called autoload.php – and that will require everything it is you need. That means all you have to do is require that file in your plugin, and you’re done.

    What? You wanted more?

    Conclusion

    Getting started with Composer isn’t harder than writing a readme, even if it’s formatted pretty weirdly. It can make including large libraries a snap. But don’t worry, you can get really complicated if you want to.

  • Protect My Plugins, Please

    Protect My Plugins, Please

    I have a site that literally requires a specific plugin always be active for things to work. It has a lot of very complicated code, and two things must never ever happen to this:

    1. It should never be disabled
    2. It should never be updated by WordPress

    Well. Okay. Let’s do that.

    A Caveat

    If you don’t have a way in place to update and secure the plugin you’re hiding, DON’T DO THIS. Once you hide it, people won’t know to update it and, in fact, won’t be able to. Because you’re also preventing traditional updates. In my case, I have a git repository that triggers a push (plus some other magic).

    Again. Unless you’ve got something set up to handle updates, don’t do this.

    Now let’s do this.

    Never Disable Me (by Hiding)

    To do this, we need to know the folder name and filename of the plugin, such as ‘my-plugin/my-plugin.php. If you want to hide the plugin you've put the code in, you can useplugin_basename( FILE )` instead, which is what I’ve done.

    add_action( 'pre_current_active_plugins', 'mysite_hide_my_plugins' );
    function mysite_hide_my_plugins() {
    	global $wp_list_table;
    
    	$hide_plugins = array(
    		plugin_basename( __FILE__ ),
    	);
    	$curr_plugins = $wp_list_table->items;
    	foreach ( $curr_plugins as $plugin => $data ) {
    		if (in_array( $plugin, $hide_plugins ) ) {
    			unset( $wp_list_table->items[$plugin] );
    		}
    	}
    }
    

    If you were writing a plugin to hide a lot of things, you would change the array to list all of them. But since this is a ‘hide myself’ deal, it’s easier to put it in the plugin itself.

    The added bonus to making the file path dynamic is that if someone gets clever and renamed the folder or file, it would still work. Neener.

    Stop My Updates

    Now this one again is easier if you put it in the plugin you’re disabling updates for, because again you can use plugin_basename( __FILE__ ) to detect the plugin. If you’re not, then you’ll need to make an array of names. But that should get you started.

    add_filter( 'http_request_args', 'mysite_disable_my_plugin_update', 10, 2 );
    function mysite_disable_my_plugin_update( $return, $url ) {
    	if ( 0 === strpos( $url, 'https://api.wordpress.org/plugins/update-check/' ) ) {
    		$my_plugin = plugin_basename( __FILE__ );
    		$plugins   = json_decode( $return['body']['plugins'], true );
    		unset( $plugins['plugins'][$my_plugin] );
    		unset( $plugins['active'][array_search( $my_plugin, $plugins['active'] )] );
    		$return['body']['plugins'] = json_encode( $plugins );
    	}
    	return $return;
    }
    

    What Does It Look Like?

    Nothing, really. You’ve hidden everything. Congratulations. Now you don’t have to worry about admins or WordPress updating your plugin. Or worse.

  • How Many Plugins Is Too Many To Create?

    How Many Plugins Is Too Many To Create?

    That wasn’t the way you expected that title to end, I bet. You were thinking “How many plugins is too many to have on my site” and that’s absolutely not the topic here. No, instead I’m asking how many plugins is too many for a developer to create?

    I Got 99 Problems …

    I think that plugins should be specific. That is, I’m not a fan of a conglomerate of plugins like Jetpack, that do a little of everything. Instead, I like a plugin that does the thing it’s supposed to do, preferably simply and well, and it moves on. That means I often have 20-30 plugins installed on a site, and that’s okay.

    At the same time, as a developer, having to support 20-30 plugins is a drain on my limited resources. Becuase here’s what I have to do:

    1. Keep up with all core changes
    2. Include and test all library updates
    3. Test with every release
    4. Update my readme
    5. Review reviews and support posts to make sure I’m not missing things

    Multiply that by 20 and it’s a lot of work. And is that work I feel like I must do?

    Gimmie One Reason …

    The reality of having plugins for WordPress, or any add on for any project, is that it’s generally thankless work and you will have more bad days than good. That’s true of many things in life, and as depressing as it can be, it’s important to keep an eye on the reality of the situation. 

    Developing software is very analytical art. You create something out of nothing, you design and test and change and tweak, and then present it to the world. Of course those days when people tell you “I don’t like the color” suck, but being humans, we discard that and grab on to the days when someone says “I love the carrot!”

    And the reality of the question at hand isn’t how many is too many, but how many are worth the work and the little reward?

    Bring it Together …

    Lately I’ve been advocating something different. Instead of making 13 separate types of gallery plugins, I’ve suggested people make one plugin for galleries and include those 13 types as display options. The amount of work is roughly the same, but it means I only have one plugin to manage instead of 13 separate readme files to edit and installs to spin up. I also have one place to look for any support posts or reviews.

    Obviously this doesn’t always work. Sometimes you have to split things up. There’s little point it combining a WooCommerce plugin with a NextGen Gallery one (unless the plugin is implementing NextGen with Woo products…). But if you can connect your projects by type, you may find out that there’s crossover. Instead of spreading your user base out over 10 plugins, you can keep them manageable with 5 to 8.

    Working For The Man …

    And what about Jetpack? It’s effectively XX separate types of plugins:

    • Writing
    • Sharing
    • Discussion
    • Traffic
    • Security

    Except when you look at that, it suddenly all connects. When I write I want to share and I want people to discuss. I also want to keep an eye on my traffic and being secure…. Okay that last one might be better off on it’s own, but it’s a suite of related apps. 43 separate apps, but they are all related when you get down to it.

    Which means even if you’re making a plugin for your company, you can probably combine it with other things safely. And that means less access and security concerns for you too, as you only have to keep track of who has access to one plugin instead of 50.

    How Many Is Too Many?

    This is as subjective as all get out, but I’ll say this. Once you personally support 20 plugins for WordPress, take a good hard look at how much time you’re spending and ask yourself… is it worth it?

    It’s okay to say no.

  • Customizing Jetpack Feedback

    Customizing Jetpack Feedback

    Fair bit of warning, this is a big code heavy post. I use Jetpack to handle a variety of things on my sites. Contact forms, stats, embeds, monitoring, backups, a bit of security (brute force prevention), and in some cases Photon and galleries. Oh and I especially use it for the tweeting and facebookieing of posts. Love it or hate it, it has it's uses and as a user, I find it easier to work with than its alternatives. However! Jetpack is not perfect. And my current drama llama was that I wanted to do two things:
    1. Show on my dashboard how many messages I had
    2. Mark a feedback message as 'answered' without deleting
    The second item was very important as on a shared-management type site, it's hard to know who did what and did everyone handle an email or what have you? Really a better tool would be something that you could reply to from within the admin dashboard, versus emailing all over, but that's another day. Instead, I decided to tackle a custom post status.

    This is NOT Fully Supported

    This is my caveat. My big warning. WordPress doesn't yet fully support Custom Post Status. That is, yes you can totally register them, but there's no easy way to put in the interface, as the trac ticket has been around since 2010 (you read that right) and it's still not done. All that said, if you're willing to wrangle a bit of Javascript and sanitize your outputs properly, you can do this.

    Big Block of Code

    <?php
    /*
    Description: Jetpack Customizations
    Version: 1.0
    */
    
    if ( ! defined('WPINC' ) ) die;
    
    /**
     * Customize_Jetpack_Feedback class.
     * Functions used by Jetpack to cutomize Feedback
     */
    class Customize_Jetpack_Feedback {
    
    	/**
    	 * Constructor
    	 * @since 1.0
    	 */
    	public function __construct() {
    		add_action( 'dashboard_glance_items', array( $this, 'dashboard_glance' ) );
    		add_action( 'admin_head', array( $this, 'dashboard_glance_css' ) );
    		
    		add_action( 'init', array( $this, 'custom_post_statuses' ), 0 );
    		add_filter( 'post_row_actions', array( $this, 'add_posts_rows' ), 10, 2);
    		add_action( 'plugins_loaded', array( $this, 'mark_as_answered' ) );
    		add_filter( 'display_post_states', array( $this, 'display_post_states' ) );
    		add_action( 'admin_footer-post.php', array( $this, 'add_archived_to_post_status_list' ) );
    		add_action( 'admin_footer-edit.php', array( $this, 'add_archived_to_bulk_edit' ) );
    	}
    
    	/**
    	 * Add custom post status for Answered
    	 * 
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	public function custom_post_statuses() {
    		register_post_status( 'answered', array(
    			'label'                     => 'Answered',
    			'public'                    => false,
    			'exclude_from_search'       => true,
    			'show_in_admin_all_list'    => true,
    			'show_in_admin_status_list' => true,
    			'label_count'               => _n_noop( 'Answered <span class="count">(%s)</span>', 'Answered <span class="count">(%s)</span>' ),
    		) );
    	}
    
    	/**
    	 * Add URL for replying to feedback.
    	 * 
    	 * @access public
    	 * @param mixed $actions
    	 * @param mixed $post
    	 * @return void
    	 * @since 1.0
    	 */
    	public function add_posts_rows( $actions, $post ) {
    		// Only for Feedback
    		if ( $post->post_type == 'feedback' ) {
    			$url = add_query_arg( 'answered_post_status-post_id', $post->ID );
    			$url = add_query_arg( 'answered_post_status-nonce', wp_create_nonce( 'answered_post_status-post_id' . $post->ID ), $url );
    	
    			// Edit URLs based on status
    			if ( $post->post_status !== 'answered' ) {
    				$url = add_query_arg( 'answered_post_status-status', 'answered', $url );
    				$actions['answered_link']  = '<a href="' . $url . '" title="Mark This Post as Answered">Answered</a>';
    			} elseif ( $post->post_status == 'answered' ){
    				$url = add_query_arg( 'answered_post_status-status', 'publish', $url );
    				$actions['answered']  = '<a class="untrash" href="' . $url . '" title="Mark This Post as Unanswered">Unanswered</a>';
    				unset( $actions['edit'] );
    				unset( $actions['trash'] );
    			}
    		}
    		return $actions;
    	}
    
    	/**
    	 * Add Answered to post statues
    	 * 
    	 * @access public
    	 * @param mixed $states
    	 * @return void
    	 * @since 1.0
    	 */
    	function display_post_states( $states ) {
    		global $post;
    
    		if ( $post->post_type == 'feedback' ) {
    			$arg = get_query_var( 'post_status' );
    			if( $arg != 'answered' ){
    				if( $post->post_status == 'answered' ){
    					return array( 'Answered' );
    				}
    			}
    		}
    
    		return $states;
    	}
    
    	/**
    	 * Process marking as answered
    	 * 
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	public function mark_as_answered() {
    
    		// If contact forms aren't active, we'll just pass
    		if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    
    			// Check Nonce
    			if ( isset( $_GET['answered_post_status-nonce'] ) && wp_verify_nonce( $_GET['answered_post_status-nonce'], 'answered_post_status-post_id' . $_GET['answered_post_status-post_id'] ) ) { 
    				// Check Current user Can and then process
    				if( current_user_can('publish_posts') && isset( $_GET['answered_post_status-status'] ) ) {
    					$GLOBALS[ 'wp_rewrite' ] = new wp_rewrite;
    		
    					$status  = sanitized_text_field( $_GET['answered_post_status-status'] );
    					$post_id = (int) $_GET['answered_post_status-post_id'];
    		
    					// If it's not a valid status, we have a problem
    					if ( !in_array( $status, array( 'answered', 'publish' ) ) ) die( 'ERROR!!!' );
    		
    					$answered = array( 'ID' => $post_id, 'post_status' => $status );
    					wp_update_post( $answered );
    				}
    			}
    
    		}
    	}
    
    
    	/**
    	 * add_archived_to_post_status_list function.
    	 * 
    	 * @access public
    	 * @return void
    	 * @since 1.0
    	 */
    	function add_archived_to_post_status_list(){
    		global $post;
    		$complete = $label = '';
    
    		// Bail if not feedback
    		if ( $post->post_type !== 'feedback' ) return;
    
    		if( $post->post_status == 'answered' ) {
    			echo '
    				<script>
    					jQuery(document).ready(function($){
    						$("#post-status-display" ).text("Answered");
    						$("select#post_status").append("<option value=\"answered\" selected=\"selected\">Answered</option>");
    						$(".misc-pub-post-status label").append("<span id=\"post-status-display\">Answered</span>");
    					});
    				</script>
    			';
    		} elseif ( $post->post_status == 'publish' ){
    			echo '
    				<script>
    					jQuery(document).ready(function($){
    						$("select#post_status").append("<option value=\"answered\" >Answered</option>");
    					});
    				</script>
    			';
    		}
    	} 
    
    	public function add_archived_to_bulk_edit() {
    		global $post;
    		if ( $post->post_type !== 'feedback' ) return;	
    		?>
    			<script>
    			jQuery(document).ready(function($){
    				$(".inline-edit-status select ").append("<option value=\"answered\">Answered</option>");
    				$(".bulkactions select ").append("<option value=\"answered\">Mark As Answered</option>");
    			});
    			</script>
    		<?php
    	}
    
    	/*
    	 * Show Feedback in "Right Now"
    	 *
    	 * @since 1.0
    	 */
    	public function dashboard_glance() {
    		if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    			foreach ( array( 'feedback' ) as $post_type ) {
    				$num_posts = wp_count_posts( $post_type );
    				$count_posts = ( isset( $num_posts->publish ) )? $num_posts->publish : '0';
    				if ( $count_posts !== '0' ) {
    					if ( 'feedback' == $post_type ) {
    						$text = _n( '%s Message', '%s Messages', $count_posts );
    					}
    					$text = sprintf( $text, number_format_i18n( $count_posts ) );
    					printf( '<li class="%1$s-count"><a href="edit.php?post_type=%1$s">%2$s</a></li>', $post_type, $text );
    				}
    			}
    		}
    	}
    
    	/*
    	 * Custom Icon for Feedback in "Right Now"
    	 *
    	 * @since 1.0
    	 */
    	public function dashboard_glance_css() {
    	if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'contact-form' ) ) {
    		?>
    		<style type='text/css'>
    			#adminmenu #menu-posts-feedback div.wp-menu-image:before, #dashboard_right_now li.feedback-count a:before {
    				content: '\f466';
    				margin-left: -1px;
    			}
    		</style>
    		<?php
    		}
    	}
    
    }
    
    new Customize_Jetpack_Feedback();
    
    What this does is create a new status for your feedback of “Answered”. Ta Dah!
  • Restrict Site Access Filters

    Restrict Site Access Filters

    I have a demo site I use to for development. One of the things I want is to be able to lock the site to logged in users only and that I can do via Restricted Site Access by 10up.

    One of the things the plugin also allows is to open up access to an IP, so someone who doesn't have an account can check the site before you go live. The problem with this feature is caching.

    Caching Restricted Pages

    It doesn't really matter what kind of caching system you use, the point is all the same. People who aren't logged in should get a cached version of the content. People who are logged in, or whom you've determined need a unique experience, don't get cached content. That's the barebones of caching.

    The problem I ran into with restricted site access is that if I whitelisted an IP range, and someone from that range visited the site, they generated a page which my cache system … cached. That meant the next person got to see the cached content.

    Worf from Star Trek face-palming

    Now this may not actually be a problem in all cache systems, but I happened to be using Varnish, which is fairly straightforward about how it works. And, sadly, the plugin I'm using doesn't have a way around this. Yet.

    Filters and Hooks

    Like any enterprising plugin hoyden, I popped open the code and determined I needed to address the issue here:

    // check if the masked versions match
    if ( ( inet_pton( $ip ) & $mask ) == ( $remote_ip & $mask ) ) {
    	return;
    }

    This section of code is checking "If the IP matches the IP we have on our list, stop processing the block. It's okay to show them the content." What I needed was to add something just above the return to tell it "And if it's Varnish, don't cache!"

    At first my idea was to just toss a session_start() in there, which does work. For me. Adam Silverstein was leery of that having unintended consequences for others, and wouldn't it be better to make it hookable? After all, then any caching plugin could hook in! He was right, so I changed my pull request to this:

    do_action( 'restrict_site_access_ip_match', $remote_ip, $ip, $mask ); // allow users to hook ip match

    The next version of the release will have that code.

    In The Field

    Now, assuming you've slipped that code into your plugin, how do you actually use it?

    Since I need to have this only on my 'dev' site, and I'm incredibly lazy efficient, I decided to put this code into the MU plugins I use for the site:

    if ( DB_HOST == 'mysql.mydevsite.dream.press' ) {
    	add_action( 'restrict_site_access_ip_match', 'mydevsite_restrict_site_access_ip_match' );
    }
    
    function mydevsite_restrict_site_access_ip_match() {
    	session_start();
    }

    This is not the only way to do it. I also happen to have a define of define( 'MYSITE_DEV', true ); in my wp-config.php file, so I could have checked if that was true:

    if ( defined( 'MYSITE_DEV' ) && MYSITE_DEV ) { ... }

    Now, you'll notice I'm using sessions, even after Adam and I determined this could be bad for some people. It can. And in my case, in this specific situation, it's not dangerous. It's a quick and dirty way to tell Varnish not to cache (because PHP sessions indicate a unique experience is needed).

    The downside is that not caching means there's more load on my server for the non-logged in user who is legit supposed to be visiting the site. Since this is a development site, I'm okay with that. I would never run this in production on a live site.