Blog

EDD Stripe v3.0 Sticks it to Ya

Easy Digital Downloads (EDD) seems to be going the route of Freemius, etc., where you can use their open source plugin(s), but not without significant overhead. Except Freemius isn’t open source. 🤷‍♀️ With only a small share of the WP users needing file management plugins using their plugin and not a competitor’s, it’s a surprising move.

Stripe Gets Pricey

If you’re not paying for their annual license, or let your license term lapse, and wish to take payments with Stripe, you’re going to pay Stripe fees, plus a 3% “application fee” which EDD funnels off your sale. This fee used to be 2% until v.3.0. Now 3% — that’s significant!

With version 3.0 of EDD Stripe, EDD moved their entire Stripe integration over to Stripe Connect, which means all the sales on your private site go through Easy Digital Downloads Connect account first. Years ago the plugin used Stripe Charge API, then moved to Payments. Now this. There’s no going back. Your EDD Stripe plugin must be licensed ($$$) or you will be paying ~ 6% on every transaction.

But there’s a pretty simple way around all this if you have the plugin pre-version 3.0 … Version was 2.9.6 the latest before the jump. Add the following PHP code to your child theme functions.php file or add it using a plugin like WP Code Snippets (front and back end).

function edds_pro_edit_intent_args( $intent_args ) {
    if ( isset( $intent_args['application_fee_amount'] ) ) {
		    unset( $intent_args['application_fee_amount'] );
	  }
	  return $intent_args;
}
add_filter( 'edds_create_payment_intent_args', 'edds_pro_edit_intent_args', 99, 1 );
add_filter( 'edds_create_setup_intent_args', 'edds_pro_edit_intent_args', 99, 1 );

add_filter( 'edds_show_stripe_connect_fee_message', '__return_false' );

If you’re using the Stripe gateway plugin version 3.0 or newer, you’ll want to take a look at the has_application_fee() method in Src/Gateways/Stripe/ApplicationFees.php … and see how they’ve got it all wrapped up tight now. ⛓️ There are no filter hooks that can be used to wiggle out of that 3% fee. However, that method can be edited to get where you need. And would have to be edited again with every plugin update.

If you’d like the help of a developer for this issue, please get in touch.

Set up a Weekly (weekend) autoresponder

Years ago I set up an autoresponder to let people know I’m probably not around on Saturday and Sunday. It’s still running, and flawlessly, so I thought I’d share how I set it up using cPanel cron and FTP/File Manager. I got this idea originally from David Heath-Whyte’s blog, but adjusted it a little bit. It might help you to see two examples if you want to set this up yourself and aren’t quite sure how. Sometimes having the same thing explained twice by two different people helps!

Note: this advice only works for people with self-hosted email. Gmail, Hotmail, Yahoo! etc., look elsewhere. Also, you’ll need hosting with access to a cPanel.

Step 1: Set up and compose your Autoresponder email

Head over to your cPanel (“control panel”). Most hosting accounts will have a control panel of some sort, and cPanel is probably the most ubiquitous type. Look for “Autoresponders” in the Email section, and click that.

When the Autoresponders page has loaded, click “Add Autoresponder.” You can only have one autoresponder for an email address, so if you already have one set up for the email address in question, just review its settings.

Below are mock settings for my autoresponder. I set the interval pretty high at 16 hours. What this means is that if someone sends me an email every hour over the weekend, they won’t get the autoresponder again until they’ve send the seventeenth email. Receiving an auto-responder every time when we already know someone is out of the office could be annoyingly inbox-stuffing. Beyond that it could create problems (such as loops) if an autoresponder replies to my autoresponder.

I have set the autoresponder to begin at 6pm this coming Thursday because in my dreams I’m taking long weekends, but you might want to switch that to something like 5pm on Friday. I stretch the weekend into Sunday evening, but stop there because most people emailing Sunday night understand that they’re not going to get a reply until Monday anyway. Those late Sunday e-mailers don’t need an auto-responder. We’d just be rubbing it in that they’re working and we aren’t!

Another common approach is to make the “From:” address a blackhole email address (e.g. no-reply@sagehen.studio or autoresponder@sagehen.studio) and indicate to people in the message that the message comes from a bot and replies will be delivered into the far reaches of another galaxy. This can help prevent autoreply loops from cluttering your inbox. Blackhole email addresses can be set up using cPanel, but that’s a whole other thing.

Step 2: Locate the corresponding autoresponder.json file

When we edit an autoresponder in cPanel, the data is saved in a JSON-formatted file. These are found in the root server folder “.autoresponder”. I want to know the absolute path (ABSPATH) of the file named email@domain.com.json. I use Transmit for FTP and it allows me the handy feature “Copy URL.” Your application probably offers something similar.

“Copy URL” gives a URL, not an ABSPATH. I know how to convert that in my head, since I’m familiar with my server. If you have no idea how to get the absolute path from a URL, then you’re in a little over your head. But no worries, a call to your web hosting company could sort it out, or good old Google could help you. In my case the absolute path to this file looks like:

/home/galuxee/.autorespond/caroline@sagehen.studio.json

Note your ABSPATH, we’ll be using it soon.

While we are in FTP, or browsing files using SSH or File Manager, let’s create a folder in our public_html/ folder called “cron”. That’s a nice way of keeping things organized, since ultimately we are creating a CRON job.

The following PHP script is what our CRON job will run each week. Each week when CRON parses this file, it will be told to move the weekend pointer forward one week, and the JSON file will be edited accordingly. Bottom line: this script is automating the process of logging into cPanel every week and editing the dates manually.

<?php

/** 
 * Set the time zone so our weekend basis is correct
 * Do not do this inside a WordPress installation
 */
date_default_timezone_set( 'America/Los_Angeles' ); // EDIT

/**
 * Set the start/end times.
 * strtotime() is a PHP function that is pretty liberal in what it will accept as a string
 * It then converts the string to a UNIX timestamp (a string of numbers equal to the number
 * of seconds since Unix Epoch on January 1st, 1970 at UTC
 */
$nextFriEve = strtotime( "next friday 18:00:00" );
$nextSunEve = strtotime( "next sunday 18:00:00" );

// Make the array which populates a healthy .autoresponder JSON file:
$auto_responder = array(
    'start' 	=> $nextFriEve,
    'interval'    => 57600, // 16 hours
    'stop' 		  => $nextSunEve,
);

/**
 * Now save the encoded array to the .json file
 * Here is where we use the ABSPATH 
 */
@file_put_contents( '/home/galuxee/.autorespond/email@domain.com.json', json_encode( $auto_responder ) ); // EDIT

/**
 * Confirmation email
 * Let's send ourselves a reassuring weekly email
 * It will confirm the autoresponder has been set up, and for what time span
 * CRON will echo this in its own confirmation email to YOU when it completes, as
 * long as you have a CRON email set up in cPanel. That's shown in the next step.
 */
echo "Howdy,\n\n";
echo "Just a reminder... your email@domain.com weekend autoresponder file was re-written for the week:\n\n";
echo "It starts on " . date( "F j, Y, g:i a", $nextFriEve );
echo "\n\nAnd ends on " . date( "F j, Y, g:i a", $nextSunEve );
echo "\n\nHave a fabulous weekend!";
echo "\n\nGenerated for you by https://domain.com/cron/autoresponder_weekend.php";

In order for this to work you at least have to edit the lines commented with “// EDIT”. Review and personalize everything else, such as the time zone, dates, interval, and the confirmation email.

Save this file as “autoresponder_weekend.php” inside your “public_html/cron/” folder. We’re almost there. Just one more step before the weekend…

Step 3: Create a CRON job using cPanel

If you’re building a website or managing email with cPanel, it’s worthwhile to get familiar with its features. Obviously. Cuz look what we’re doing now! We’re practically ruling the universe! What I’m trying to say is cPanel is a very powerful tool. As is CRON. Together they can create a “smart home” out of your hosting account. Use them wisely.

We are going to set up a CRON job. If you already have CRON jobs, you’ll want to set this one up to run at a different time than the others, esp. if the others are heavy processes. Mine is set up to run two minutes after my other CRON job, and away from when I run backups on my server (in the middle of the night), just to give the processes a little space. ???? You don’t have to do this. I just try to be nice to my computer since it’s doing me this big favor of running automatic auto-responder updates for me every week.

In the image above you can see that I’ve got an email set up for the autoresponder. If you do not want a confirmation email for this particular CRON job you can use the “/dev/nul 2>&1” command:

/user/bin/php -q /home/galuxee/cron/weekend_autoresponder.php > /dev/null 2>&1

I like getting the weekly emails as they get me pumped for the weekend. My CRON job runs at 14:02 every Thursday afternoon. The weekend is coming!

The command points to the PHP script we wrote and saved earlier.

Depending where the PHP library is kept on your server, your command might look like:

php /home/galuxee/domain/cron/weekend_autoresponder.php

or even

It sort of just depends on what you are doing. The “-q” command assumes that your PHP file is solid and skips any output generated by faults, so I use it, since the code is so simple. If you are having troubles you might want to omit that and check your debug logs.

If you can’t wait to test this setup, you can speed things up. Change the CRON job to run every few minutes, then open up your .autoresponder JSON file to see if the weekend dates have changed (in UNIX format; here’s a handy UNIX time converter). You can corroborate the change in your cPanel autoresponder settings.

Happy Weekend!

Awesome. I hope this helps you put aside work and have a relaxed weekend, every weekend. Maybe it also helped you learn a little more about cPanel and CRON jobs, too.

And finally: thanks, David!

Fix Easy Digital Downloads to Increase Sales

Over the years I’ve sure appreciated having Easy Digital Downloads (EDD) plugin to platform my plugin sales. It’s pretty cool, and handles most my needs including software licensing.

However, I have had numerous little issues with the plugin. I apply each update with trepidation, because often changes (“upgrades”) cause abrupt slumps in my sales. Of course the developers are well-meaning, and I’m sure these upgrades are in fact upgrades for certain people. But it’s becoming more and more clear that even though I’m a paying subscriber of EDD, their developers might not be “in touch” with the my reality or my customers’ realities.

There are things a customer just. does. not. want. to. see. when. shopping.

Case in Point

With the latest updates to the Stripe payment gateway plugin, EDD moved to the new API and allows small text to appear on screen just below the credit card fields, which easily would scare off a good number of customers:

“By providing your card information, you allow [Business Name] to charge your card for future payments in accordance with their terms.”

Oh dear God! How long has my website been telling people THAT? “Their terms?” What terms? Where does the customer now go to find those terms? Away from your checkout, that’s where.

Let’s Fix THAT

I know that EDD is stressing the subscription model and has a strong integration with Stripe for that model, but for those of us running EDD shops without subscription, this kind of small print is… not great. Luckily there’s an easy fix, but it does involve a few lines of PHP

add_filter( 'edds_stripe_payment_elements_terms', function( $terms ) {
    $terms['card'] = 'never';
    $terms['applePay'] = 'never';
    $terms['sofort'] = 'never';
    $terms['cashapp'] = 'never';
    $terms['bancontact'] = 'never';
    // view more methods here: https://stripe.com/docs/js/elements_object/create_payment_element#payment_element_create-options-terms 
	return $terms;
} );

Other payment methods might need to have these off-putting terms turned off too. You can view their keys here: https://stripe.com/docs/js/elements_object/create_payment_element#payment_element_create-options-terms

My Verdict

The card terms should be set to “never” by default, and set to the other ☠️☠️☠️ option when EDD store subscriptions are enabled. Developers should be serving the majority of their users, esp. their paying users. I can’t imagine that most EDD users are using the subscription model. It’s expensive, complex and controversial amongst merchants and customers.

After various other SNAFUs over the years, makes me very hesitant to apply site upgrades on anything but a development server. This is frustrating. I just don’t like the prospect of combing through upgrades to review UX each time. I’m paying them so it just doesn’t seem like it should be my job. </tirade>

Bridging the Gap between PHP 5.3 and 8.2

This week I got finally got involved with maintaining the languishing TCPDF library on Github. Long overdue. Embarrassingly long overdue, since I’ve based one of my businesses on this open source gem for nearly ten years now.

The author, Nicola Asuni, has been working for many years on a successor to TCPDF, but it is stalled… likely due to the usual reasons: life and cash. I can relate. I’ve been sitting on a huge WP plugin project for two years now, desperately trying to move it forward while life happens and I’m forced to prioritize support for my paying customers.

A couple years ago an eager young programmer, William Desportes, hopped onto the TCPDF GitHub and made an incredible fork which would have really helped bring TCPDF into this decade. It needed massaging to work with PHP 8.0+ and he gave it a rub. However, understandably, the original author reminded Desportes he was already working on a new version, and that helping with/watching the fork would just be too much. Probably annoying, too.

Asuni invited Desportes to be a project contributor on the old TCPDF. Since, Desportes has been the most active, if not the only active contributor to TCPDF, with Asuni popping in to approving PRs. And largely thanks to Desportes, the library is keeping up with huge PHP updates.

Anyway, in my testing I discovered a bug in TCPDF while testing with PHP 8.2:

Warning: use of “self” in callables is deprecated

Uh oh. This means we cannot use self::method as callables inside functions that call them, like array_map(), any longer in PHP 9.0. We also cannot do a thing where we name the class and method in an array, e.g. array('class', 'method')

We have a little bit of time to fix this before PHP 9.0 comes out, but everyone hates a warning so best fix it now. An instant replacement for using self::method would look like:

self::class . "::method"

Another option could be something like:

[self::class, "method"]

Neither of these will work with PHP 5.3. The moment PHP 5.3 sees ::class, it breaks. The moment PHP 5.3 and 5.4 see square brackes, they break.

So how the hell do we get our fix to work with both PHP 8.2 AND PHP 5.3 (TCPDF is stated compatible down to PHP 5.3)?

Issue: https://github.com/tecnickcom/TCPDF/issues/632

Corresponding Pull Request: https://github.com/tecnickcom/TCPDF/pull/633

I was pretty excited to patch this up quickly, but my initial fix was no good. Somehow I got lost in testing and was led to believe it worked in PHP 5.3 But ::class isn’t a thing until PHP 5.5. Back to the drawing board! I do not and cannot use a test for PHP version because:

  • Moving the code for newer versions to newly created files is cumbersome and feels backward-moving
  • Again, the instant PHP 5.3 sees code it doesn’t recognize, e.g. ::class or [] notation, it just breaks

So I spend some time testing various things, trying to find a reference to the static method that both PHP 5.3 and PHP 8.2 will use unbegrudgingly. And after trying various methods and going back and forth between 5.3 and 8.2 and all versions in between, I find that using `get_called_class` works as a replacement for array(‘TCPDF_FONTS’, ‘unichrUnicode’), especially these static methods I’m looking to resolve in TCPDF. Thus

return array_map(array('TCPDF_FONTS', 'unichrUnicode'), $ta);

becomes:

 return array_map(get_called_class() . '::unichrUnicode', $ta );

I’ve sorted it out, but in the process I sorta lose my marbles and start posting about nuts on Github. I’m battling the frustration of WHY ARE WE STILL coding for PHP 5.3 when it was deprecated 9 years ago?! I’m also battling some imposter syndrome. Like, why am I the chosen one today? I hardly know how to use Github! How can I be one of the only people left working on this massively popular library? Will this work for everyone the way it worked for me in test after test, or will this be my “Carrie at the prom” moment, having finally just stuck out my neck on Github? Time will tell! But for now, it works, so… you’re welcome. ☺️

WooCommerce product add to cart consent pop-up

WooCommerce handles its add-to-cart process with AJAX, which is great and all, but how do we interrupt that AJAX process to add in our last-minute validation/verification? Inside woocommerce/assets/js/frontend/add-to-cart.js line 73, there’s the code:

<?php
// Allow 3rd parties to validate and quit early.
if ( false === $( document.body ).triggerHandler( 'should_send_ajax_request.adding_to_cart', [ $thisbutton ] ) ) {
    $( document.body ).trigger( 'ajax_request_not_sent.adding_to_cart', [ false, false, $thisbutton ] );
    return true; 
}

Cool! WooCommerce developers put that there to help us intercept add-to-cart requests. That means all we’d need to do is write some JavaScript to return false for the ‘should_send_ajax_request.adding_to_cart’ triggerHandler(). Something like this:

jQuery( "body" ).on( "should_send_ajax_request.adding_to_cart", function( e, $button ) {
    if ( $( "body" ).hasClass( 'our-condition-is-not-met' ) ) { // random example
        return false;
    } 
    return true;
});

This would work in a lot of scenarios, but is limited to frontend scripting and gives no access to the server, so it’s a bit handicapping. Plus, we are already stuck inside the WooCommerce add-to-cart JavaScript, so we can’t run other scripts…

I wanted to bring up a JavaScript window.confirm() dialog box asking the customer to confirm something before the item could be added to the cart. By the time this trigger is fired, it’s too late to run a dialog on screen. So I did something a little tricky.

I added a new “fake” add-to-cart button (with CSS class “custom_add_to_cart_button”) to the product page, and hid the real WooCommerce add-to-cart button. The fake button would trigger the dialog, and if the customer confirmed, I’d trigger a click of the real button, which could stay hidden.

I only wanted to do this on a specific WooCommerce simple product (ID #5653), and so I edited the woocommerce/single-product/add-to-cart/simple.php template:

<?php if ( is_single( 5653 ) ) { ?>

    <button type="button" class="custom_add_to_cart_button button alt<?php echo esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '' ); ?>"><?php echo esc_html( $product->single_add_to_cart_text() ); ?></button>

    <button type="submit" name="add-to-cart" value="<?php echo esc_attr( $product->get_id() ); ?>" class="d-none single_add_to_cart_button button alt"><?php echo esc_html( $product->single_add_to_cart_text() ); ?></button>

<?php } else { ?>

    <button type="submit" name="add-to-cart" value="<?php echo esc_attr( $product->get_id() ); ?>" class="single_add_to_cart_button button alt"><?php echo esc_html( $product->single_add_to_cart_text() ); ?></button>

<?php }

Since I’m using Bootstrap you can see how I hid the native “.add-to-cart” button using the “d-none” class. When not using Bootstrap you could just hide the native WooCommerce .single_add_product_to_cart button using CSS:

.postid-5653 .single_add_to_cart_button{display:none}

Instead of editing WooCommerce templates, which can lead to headaches down the road if WooCommerce makes breaking changes — or even if they just keep updating the template — this could also be done by using the 'woocommerce_after_add_to_cart_quantity' action hook and the $product global, like so:

<?php add_action( 'woocommerce_after_add_to_cart_quantity', 'custom_button_after_add_to_cart_quantity' );

function dummy_button_after_add_to_cart_quantity() {
    global $product;
    if ( ! $product || 5653 !== $product->get_id() ) {	
        return;	
    } ?>
			<button type="button" class="custom_add_to_cart_button button alt<?php echo esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '' ); ?>"><?php echo esc_html( $product->single_add_to_cart_text() ); ?></button>
<?php }

Finally, I queue up my custom jQuery to handle the add-to-cart click and show the confirmation dialog. If the customer doesn’t confirm, nothing happens. Otherwise, the real WooCommerce add-to-cart button is clicked and the item is added to the cart (assuming it is in stock, etc).

jQuery(document).ready(function($){
    $( ".custom_add_to_cart_button" ).on( 'click', function(e, $button) {
        if ( ! window.confirm( "I understand this product is likely to blow my mind. I am prepared!" ) ) {
            return;
        } $( '.single_add_to_cart_button' ).trigger( 'click' );
    });
});

Sometimes we just have to get a little tricky to make customizations to WooCommerce shops without over-complicating things. Adding another button to the product page doesn’t hurt anything. Worst case scenario is someone, maybe someone using a screen reader, clicks the real WooCommerce button (because though it is hidden, it is still in the DOM) and proceeds directly to the cart without interacting with the dialog. That would be a very rare case, or might happen in situations where your dialog is enforcing something people don’t want enforced (in which case they will seek work-arounds in the DOM). Anyway, in my case it’s really not something to worry about; I’m just pointing out that this solution, like most online consents, is not bullet-proof.

All said sometimes there are specific details or disclaimers about our WooCommerce products that we really need customers to know about before they buy. Examples might be:

  • “I understand this item has a 3-week lead time,” or
  • “I understand this product is not recommended for children under the age of 8”, or
  • “I understand that this product can be damage vision and cause blindness if shined into eyes”

Forcing customers to consent to these important points can prevent heartache, disputes and returns, so often it’s worthwhile to program in a dialog. If you have a lot of products it might be worth using a plugin for this feature, such as Product Disclaimer for WooCommerce. But especially if you just need this for one product it’s worthwhile to just write out a dozen short lines of code or so to get it done, like I did.

Easy Digital Download Confusion

Once a customer has purchased more than one download from your Easy Digital Downloads (EDD) e-commerce shop, and especially if they have renewed their purchases year-over-year, there will be a source of confusion for them in their account pages, if/when they go to fetch a download file.

The list of downloads will include duplicate links in table rows, and the customer might not know which link to click to get the latest file. In fact, all the links with the same title are the same download. But how are they to know that? Why should they have to suss all that out, when it should just be a quick grab-n-go?

I get asked about this repeatedly, so I decided to code in a fix. I initially thought I’d solve this problem using the ‘edd_before_download_history‘ action hook in the ‘easy-digital-downloads/templates/history-downloads‘ template file, but that action hook doesn’t include the $orders variable, and that’s a shame because I can’t conditionally show a message if the customer has more than one download, e.g.

if ( count( $orders ) > 1 ) {
    // Then show an awkward message, like:
    echo '

awkward message explaining how all the links below with the same title are actually to the same file.

'; }

I’d have to put my awkward message in the template if I wanted to be at least minimally awkward about my message (I mean, only showing it to customers with more than one order). Sigh.

And so I ended up uploading a template override after all, which I try to avoid because updating templates every time EDD updates the parent templates is a pain. That said, I’ve updated EDD and WooCommerce templates so many hundreds of times I could almost do it with my eyes closed. That doesn’t mean it’s fun. I wish EDD would include some $variables in more of its action hooks.

Here’s what I did. I added some logic such that the $orders variable contains orders by DATE, in descending (DESC) order. That way, when the orders are looped through in the template, as they already are, I can collect item product IDs in an array. If that item product ID is already shown, I won’t show it again:

First, add two lines after line 31, to add to the edd_get_order query args:

'type'           => 'sale',
'status__not_in' => array( 'trash', 'refunded', 'abandoned' ),
'order'          => 'DESC', // add this line
'orderby'        => 'date_created', // add this line, too!

Next, add one line after line 51, so it looks more like this:

$prod_ids = [];
foreach ( $orders as $order ) :

Finally, add 6 lines of code after line 53, so it looks like this:

foreach ( $order->get_items_with_bundles() as $key => $item ) :

    if ( in_array( $item->product_id, $prod_ids ) ) {
        continue; // download link is already shown
    }
    if ( $item->is_deliverable() ) {
        $prod_ids[] = $item->product_id;
    }

Makes it much easier for a customer to just get their download without guesswork, which is what EDD is all about, right?

Prorated Software Upgrade Crack

It’s amazing how some customers’ behavior can highlight innovative ways of saving money.

It never occurred to me until I started selling software, and managing annual software licenses, that there’s a pretty simple way to save money when trying to renew software licenses, without a coupon. Many businesses pro-rate license upgrades, and prorate them by date, such that the closer you come to expiration, the cheaper an upgrade gets. So a frugal move to make is to wait until just days before your software license expires, then upgrade to the next level up… for a tiny fraction of what it would cost to renew the lesser license currently held. Voilà – we now have better access for another year, for cheaper than a straight renewal!

While this is pretty cool for the customer, this is a big problem for software sellers that causes significant loss of income. In particular it’s a weakness for some Easy Digital Downloads (EDD) admins using EDD’s Software Licensing extension. I wrote a PHP code snippet for use with EDD Software Licensing. It locks out prorated upgrades after a certain number of months of license ownership (3 months):

/**
 * Customers lose prorate upgrades if less than 9 months left on license
 *
 * @param float|int $prorated Calculated prorated price
 * @param int $license_id ID of license being upgraded
 * @param float|int $old_price Price of the original license being upgraded
 * @param float|int $new_price Price of the new license level
 * @return float|int
 */
function my_get_pro_rated_upgrade_cost( $prorated, $license_id, $old_price, $new_price ) {

    if ( ! class_exists( 'EDD_Software_Licensing' ) ) {
        require_once WP_PLUGIN_DIR . '/edd-software-licensing/includes/classes/class-edd-software-licensing.php';
    }
    // Get license object from license ID
    $license = EDD_Software_Licensing()->get_license( $license_id );
    if ( 'lifetime' === $license->expiration ) {
        return $prorated; // why lifetime licenses should get caught in this hook is beyond me
    }
    // 23670000 is the number of seconds in 9 months, change ad lib
    if ( ! is_object( $license ) || $license->is_expired() || ( ! $license->is_expired() && ( (int) $license->expiration - time() < 23670000 ) ) ) {
        return $new_price; 
    }
    return $prorated;

}
add_filter( 'edd_sl_get_pro_rated_upgrade_cost', 'my_get_pro_rated_upgrade_cost', 11, 4 );

Should a customer decide they need to upgrade the software, they need to do it soon after purchase, not as a way to obtain a deep discount. In the case of this EDD add-on snippet below, they need to upgrade within 3 months, otherwise prorating is not offered. If they try to upgrade an expired license, prorating is not offered.

I hope this helps you. If it does please reach out to say hi!

Ways to harden your WordPress website without using bulky plugins

You can add security plugins to your website, but if you don’t know exactly what they’re doing, not only can you silently break things, but you can slow down your site with a lot of extra code. They’re great, but it’s not like there isn’t risk in blindly activating any plugin. I develop plugins and try to predict how things can go wrong, so I know just how bewildering the number of things that can go wrong feels. When I can simplify things on my own sites, I always go that route.

I recently decided to stop using a security plugin because I’ve implemented security measures of my own, and my server handles SSL redirection. I love the Really Simple SSL plugin but didn’t need it in this particular instance. Really Simple SSL had a couple nice features I decided to just hardcode into my functions.php file. Here are a couple of them:

Disallow ‘admin’ Usernames

WordPress includes a quick and easy filter hook, called “illegal_user_logins”, for adding disallowed WordPress usernames. You’ll want to disable ‘admin’ and ‘administrator,’ and any other predictable usernames for your setup (such as your domain name and nickname). If you’re already using these usernames, you’ll want to change them in your MySQL database _users table first. This hook defines elements of an array. The following code adds an ‘admin’ element to that array:

/**
 * Make the 'admin' login inaccessible to new users
 *
 * @param array $illegal_logins
 * @return array
 */
function make_admin_user_login_illegal( $illegal_logins ) {

    $illegal_logins[] = 'admin';
    $illegal_logins[] = 'administrator';
    return $illegal_logins;

}
add_filter( 'illegal_user_logins', 'make_admin_user_login_illegal', 9, 3 );

Restrict User Enumeration

Hackers can type in your URL followed by /?author=1 (because the admin is usually user #1) and find out your username, which gives them half the information they need to force a hack. They can do this on any other user, starting with the number 2, your second registered WordPress user. Make usernames hard to guess, and keep them from finding them so easily with this PHP code, taken from Really Simple SSL:

/**
 * Prevent User Enumeration
 *
 * @return void
 */
function check_user_enumeration() {
    if ( ! is_user_logged_in() && isset( $_REQUEST['author'] ) ) {
        if ( preg_match( '/\\d/', $_REQUEST['author'] ) > 0 ) {
            wp_die( sprintf( 'Forbidden - number in author name not allowed = %s', esc_html( $_REQUEST['author'] ) ) );
        }
    }
}
add_action( 'init', 'check_user_enumeration' );

The Most Common WordPress Mistakes People Make

For several years I’ve been scanning the support forums at WordPress.org and helping where I can. I’ve been using WordPress since it came out in 2003, and because it’s at least partly free and open-source, I like to contribute. Aside from the obvious — helping others — contributing helps me keep abreast of what’s new in WordPress and hones my skills. On top of occasionally answering questions in the general support forums, I also wrote and support two plugins in the wordpress.org plugin repository.

I frequently see quite a bit of misunderstanding on a basic level about what WordPress is and what it can do, and I’d like to toss in my thoughts about that here. I hope I can help clear up a few reasons some people think “WordPress sucks.”

WordPress is WordPress is WordPress

Not true. WordPress has two arms: WordPress.org is the open-source, free arm of WordPress. WordPress.com is not open-source or free; it is essentially a website hosting service.

Unfortunately when WordPress is recommended to people, some do not understand there is a big choice to make between the two arms. They end up at either wordpress.org or wordpress.com (hopefully now you see the difference in the URL now) and do not realize they’re only seeing part of the picture. When they are disappointed, they often throw baby out with the bathwater.

When people say, “WordPress sucks,” I ask if they are using wordpress.org or wordpress.com. When they grow puzzled, I’ve found another soul lost in WordPress world. Ideally, WordPress would make the two arms of their empire more distinct by perhaps renaming one. Heaven forbid!

I inhabit wordpress.org land, where in general things are affordable and anything is possible. When people say, “WordPress sucks,” I would hope they would make sure to use wordpress.org. I have never recommended wordpress.com to anyone. If you’re going to use wordpress.com, you might as well use WIX. Both end up being expensive and neither gives you much control.

Two upraised hands against sky backdrop, one with open pair of handcuffs attached

Everything WordPress Should be Free

This is a bit absurd, because humans have to write and support all the millions of lines of code that go into making the WordPress world spin, and those people need food on the table, too.

Open source and “free” do not mean that support is free, or that all supplementary code is free. Those words simply mean that you are free to take the code and use it. An example of closed source and not free would be Photoshop. Photoshop is expensive and we are not allowed to “look under the hood” of the Photoshop application. If WordPress were like PhotoShop, imagine where the web would be now! Instead, as of October 2021, WordPress is used by 42.8% of the top 10 million websites in the world.

More and more on the wordpress.org support forums, I see people asking for help they should be paying for. It is one thing to submit a ticket when a bug is maybe identified, or to ask for help understanding a concept, or for general direction, but it is another to ask the open source community to provide free coding support, especially for a commercial project. WordPress might be one of the most well-documented pieces of software around, and chances are that in the past 20 years, your question has already been answered. I learned by “Googling it,” and I continue to Google my questions about WordPress every day. Folks asking other people to essentially do their Googling for them will eventually be rebuked or come to feel some healthy shame for having taken advantage of others’ kindness.

As a professional making a living off WordPress, I appreciate and respond best when people respect that my knowledge isn’t any less valuable than the knowledge of a plumber, electrician, baker, or musician. Everyone needs to get paid in order to get by. And ultimately, we get what we pay for.

WordPress “Experts…” Who Are Not Experts

First of all, be leery of ANYONE who calls themselves “expert.” Because to me it’s obvious that ANYONE can call themselves expert, to the point where the word is meaningless anymore. What are their ACTUAL credentials? Can they prove they did the work?

There are quite a few people who spend a lot of time at the keyboard using WordPress, and who decide to take a crack at making money with it. Unfortunately many of them don’t know what they don’t know. There are actually people out there who don’t even realize WordPress isn’t run off a file system alone. People who actually believe that WordPress can be entirely managed by clicking buttons and installing plugins!

Be careful to not hire an imposter. Make sure when you hire an “expert” that the expert is able to code in the languages of WordPress: PHP, JavaScript, HTML, and CSS. If your WordPress “expert” can write a couple iffy lines of CSS, yet has no idea what a “hook” is or how to write a few good lines of PHP to customize your website, keep looking.

There are a lot of unwitting imposters out there who should call themselves WordPress “administrators,” and not WordPress “developers”. This has turned the WordPress economy into an unsustainable “race to the bottom,” and further perpetuates the misconception that “WordPress sucks.” It’s unfortunate for everyone involved, but especially the unwitting customer.

Imposter “developers” will load up your WordPress installation with dozens of plugins including hundreds of thousands of lines of code in order to get simple tasks done when in most cases a couple dozen lines of hand-written code would suffice. They are unaware of how these plugins and unnecessary extra code slow down your site. To “fix” a slow site, they’ll add even more plugins. Eventually your site will break, and they will hire someone else to fix it. This should not happen.

There are no official certifications to prove a “developer” status in the WordPress world, so feel free to ask your developer to see some code samples. Have they written a custom WordPress theme or plugin? They should easily be able to show you something they built from scratch, and prove it is their work.

carpenter at work bench takes a moment to research something on the laptop

WordPress is DIY

Absolutely! It can be! If you love to tuck into big projects and are an autodidact, go for it. That’s what I did starting in the late 1990s. I built silly blogs and simple websites, copied website features I thought were cool into my sites until they made sense, learned HTML and CSS, and finally discovered WordPress in 2003. Since then I have learned other languages, too. But let me tell you, learning how WordPress works and how to code for it didn’t happen overnight, or in weeks, or even in years. Unless you are going to study it round-the-clock for months, it is going to take years to become a WP developer.

In the meantime, I strongly recommend that everyone with a website hire help. Especially if your website is valuable to you, or an e-commerce or commercial website, hire qualified help! Also get help if you want your website:

  • to rank on search engines
  • to be (handicap) accessible
  • to be easy-as-possible to look at and use
  • to be secure and safe for you and users
  • to have custom features, such as automating signups or bookkeeping
  • to build a reputation and make money

These are features plugins might claim to do, but in my experience, new WordPress users regularly fail to set up plugins correctly so their site is in fact working for them. Your website should work FOR you, not against you.

There are predictable things that new WordPress users and imposter “developers” will do to a website which will absolutely negatively affect both how that website ranks on the web and how users interact with it. If you want your website to be seen and make money, hire help. Yes, there’s a slight chance that your content is so amazing or viral that your site will grow successful despite itself. (When it does, hire qualified help.) But most unmanned, unprofessional sites will simply loiter the web and corrode, ultimately diminishing someone’s reputation.

There are basically a couple levels of professional WordPress help:

  1. WordPress Administrators
  2. WordPress Developers

If you insist on not hiring a WordPress professional, it’s wise to get up to at least Administrator speed yourself. Know what the buttons do. Know when to press them. Learn where to draw the line, where to ask for help, and when to pay for help.

WordPress Definitely Does Not Suck

That’s it! I find that some foundational misunderstandings about WordPress is what causes some people to think it sucks. I believe that if it seems to suck, it’s almost always a user error. I truly believe that anything can be done with WordPress. (Somethings shouldn’t be done, but they could!) If someone tells you something can’t be done with WordPress and they’re not a high level programmer, don’t believe them — ask a programmer!

If you are frustrated with WordPress and having trouble finding answers, please get in touch. It could be that at some point you simply took a wrong turn. I’d love to get you back on the map.

Can AI Take Over Building WordPress Plugins?

Can artificial intelligence engines replace WordPress developers?

Sure, maybe it could, but then who would share and support the plugins, and how would regular folks know where to turn when things veer off target? 😉 Beyond that, you wouldn’t get to email your lovely customer support rep and get personal attention, or even trust the attention was “personalized.”

I’ve been wondering about the impact artificial intelligence will have on experienced WordPress developers like myself, given I already deal with quite a bit of shade from folks who doubt my skills. Often people mistake me for my own “secretary,” not the developer I am. Some people throw shade obliquely by advertising their services as WordPress developers when in fact they are WordPress administrators/users — and cannot write or understand code, or only do it on a basic level. Developers are fluent in at least one programming language, period. Other people inadvertently throw shade and often sabotage themselves by hiring these people and others who charge much less than skilled developers, and barely eke out an acceptable product. I’m looking at you, Fiverr clients.

I’m not the only one concerned about this disturbing market trend, and I’m not ashamed to complain about it myself once in a while. I’ve worked very hard to learn several programming languages, and I add to my knowledge about the Web nearly every day. The threat of being overpowered by imposters is compounded by the threat of being overpowered by AI. When I hear that AI often writes elegant code solutions, my interest is totally piqued! But evidently, these machines still need to be paired with developers for code review to make sure projects stay safe and on target. And because of this, I’m hoping that more WordPress users shopping for coding help realize that cheap help should be paired with actual developer help for code review. Scrimping on WordPress projects can set a site up for slow speeds, bugs, security holes, and unpredictable functioning. I remain optimistic that this AI development will cause a reckoning for the most scrupulous people out there.

You know why I’m not worried? It’s because there are still great folks out there who understand the value of a human touch. They pay a little more for better product, and understand real value. (Value isn’t the price, it’s the product/service.) I’m looking at you! Thanks so much, customers!

Trying out AI (Bard)

Anyway, I took Google Bard for a spin today to write a sales pitch for “the best PDF watermarking plugin for WordPress.” I’ve edited it very slightly. Here goes:

The WooStamper PDF plugin is the best way to protect your PDF files and ensure that they are only accessed by authorized users. With this WordPress plugin, you can watermark your PDFs with customer contact information, or any other text that you want to display. You can also password protect your PDFs, so that only authorized users can open and/or edit them.

The WooStamper plugin is easy to use and requires no coding or design experience. Simply upload your PDFs to your WordPress site and then use the plugin to watermark and password protect them. The plugin will automatically generate a watermarked and/or password protected PDF for each file that you upload.

The WooStamper plugin is a powerful tool that can help you protect your PDFs and ensure that they are only accessed by authorized users. If you are looking for a way to watermark and password protect your PDFs, then WooStamper is the perfect solution for you.

Here are some of the benefits of using the WooStamper plugin:

  • Protect your PDFs from unauthorized access. The Watermark WordPress Files plugin allows you to watermark your PDFs with your logo, contact information, or any other text that you want to display. This makes it difficult for unauthorized users to access your PDFs.
  • Prevent unauthorized downloads. The Watermark WordPress Files plugin allows you to password protect your PDFs. This means that only authorized users will be able to download your PDFs.
  • Increase brand awareness. Watermarking your PDFs with your logo or contact information can help to increase brand awareness. When people see your watermark on a PDF, they will be reminded of your brand.
  • Improve customer satisfaction. By providing your customers with password protected PDFs, you can improve customer satisfaction. This is because your customers will be able to download your PDFs without having to worry about unauthorized access.

If you are looking for a way to protect your PDFs and ensure that they are only accessed by authorized users, then the WooStamper is the perfect solution for you.

Wow. Redundant much? I actually wonder if this post damage my SEO by repeating key terms so much! But there you have it: a big garbage in garbage out machine! I know that were I to feed it more data and a better query, I could get somewhat more detailed and human-sounding text, but this was just a quick and dirty test. Could AI write documentation? Sure, but again: garbage in, garbage out. Could AI respond to support requests? Sure, but again: same, garbage in, garbage out. I expect that AI is more likely to speed our slide into another dark age than anything as humans rely more and more on junk information, and become less and less able to tell where information came from. This could be an entertaining show/battle to watch! ????????