Samuel Elh Blog

WordPress, PHP, Python and JavaScript tutorials and snippets

MailChimp WordPress Feed – Adding a Featured Image and a Read More Button

In this quick tip, we’ll learn about tweaking our blog posts feed in order for MailChimp RSS-to-Email service to fetch it and actually parse the posts featured images and a read more button that links to the actual post.

If you don’t have any RSS-to-email campaign yet, you can learn more about sharing your blog posts with MailChimp via RSS feeds.

We will tweak WordPress RSS feed to display some HTML code which consists of the post featured image, post excerpt and a read more button. Now we want this to occur only for MailChimp and we don’t want these elements to show for our feed readers, so we’ll add a query parameter to our MailChimp Feed URL so we can base our code on it.

Edit RSS feed URL setting of your campaign and append ‘?mailchimp_feed_rss=1’ to it. For example if it was ‘http://example.com/blog/feed/’, then it should become ‘http://example.com/blog/feed/?mailchimp_feed_rss=1’.

The above step is required, otherwise the feed filters will not be applied.

You can as well ignore posts from certain categories not to be parsed within the query of our MailChimp feeds, you can pass the categories identifiers to the `feed_ignore_categories` global in the plugin code.

Here’s the full code in a plugin:

<?php
/*
Plugin Name: Tweak MailChimp Feeds RSS
Plugin URI: https://samelh.com/blog
Description: Tweak MailChimp RSS Feeds to add the featured image, excerpt and a read more button.
Author: Samuel Elh
Version: 0.1
Author URI: https://go.samelh.com/buy-me-a-coffee
*/

defined ( 'ABSPATH' ) || exit ( 'Direct access not allowed.' . PHP_EOL );

$GLOBALS['feed_ignore_categories'] = array( 
    /**
      * To ignore posts from certain categories,
      * enter your category IDs here separated by commas. example
      * 2, 19, 28
      */
);

function filter_the_content_feed( $content ) {
    global $post;

    $featured_image = has_post_thumbnail($post) ? get_the_post_thumbnail($post) : null;
    
    $excerpt = trim( wpautop(get_the_excerpt( $post->ID )) );

    $read_more = '<table border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;background-color:#25aae2; border-radius:5px;">';
    $read_more .= '<tr>';
    $read_more .= '<td align="center" valign="middle" style="color:#FFFFFF; font-family:Helvetica, Arial, sans-serif; font-size:16px; font-weight:bold; letter-spacing:-.5px; line-height:150%;">';
    $read_more .= '<a href="' . get_the_permalink($post) . '" target="_blank" style="padding: 15px 30px; line-height: 40px;color:#FFFFFF; text-decoration:none;">Read More</a>';
    $read_more .= '</td>';
    $read_more .= '</tr>';
    $read_more .= '</table>';

    return $featured_image . $excerpt . $read_more;
}


function pre_get_posts_mailchimp_rss($query) {
    $query->query_vars['category__not_in'] = $GLOBALS['feed_ignore_categories'];
}

if ( isset($_REQUEST['mailchimp_feed_rss']) ) {
    remove_all_filters('the_content');
    add_filter('wpseo_include_rss_footer', '__return_false');
    add_action('pre_get_posts', 'pre_get_posts_mailchimp_rss');
    add_filter( 'the_excerpt_rss', 'filter_the_content_feed', 999 );
}

You can download this plugin from Github.

Bulk Download Wistia Account Videos

In this quick tip, we’ll learn about downloading your Wistia account videos in batch, and saving to your local or preferred drive.

We’ll be using Python for this as it is easy and quick to implement, but you can also use the scripting language of your choice.

As there is no official feature from Wistia that covers this, their API suggests that this is possible, as you can obtain a full list of your videos and their metadata following this route:

https://api.wistia.com/v1/medias.json?api_password=xyz123

The video metadata include the video name, direct download link in different formats (we’ll focus on MP4 for this tutorial), which you could gather and then iterate through the list to download everything.

It’s also worth mentioning that you should add pagination arguments to the API route, in order to obtain 100 videos per page which is the maximum value.

First we want to get the full list of videos.

To get the full list of videos (which we’ll refer to as items), you’ll want to iterate through all the available pages and gather the items.

Here’s the code necessary to do this:

import urllib2
import json

api_token = '<REST-TOKEN>'

def fetch_videos(page):
    try:
        return urllib2.urlopen('https://api.wistia.com/v1/medias.json?per_page=100&page=%d&api_password=%s' % (
            page, api_token
        )).read()
    except Exception, e:
        pass

items = []

for i in xrange(1, 99):
    print "Fetching videos at page %d.." % i
    res = fetch_videos(i)

    if not res or not len( str(res) ):
        print "Pagination has stopped at %d." % i
        break;

    data = json.loads(res)

    for item in data:
        items.append({
            'name': item['name'],
            'url' : item['assets'][0]['url']
        })

print 'Successfully fetched %d items.' % len(items)
print 'Saving to items.json file..'

with open('items.json', 'w') as outfile:
    json.dump(items, outfile)

Please insert your API token (password) in the place of `<REST-TOKEN>`, which you can create from your Wistia account.

After saving the code into a given file, say `get_video_urls.py`, we’ll run it with Python to fetch the items:

python get_video_urls.py

And it should create a JSON file named items.json which contains the videos and their titles and direct download links. That’s a good start.

Downloading Wistia videos

Next up is downloading the videos. This can be faster or the opposite, it all depends on your internet connection speed, and also the size of the downloads. But you can download videos in batches and create multiple processes, each process handling a group of videos, say for instance 10 processes to download 1000 videos quickly (which may take 1-2 hours). You can use screen or tmux to have multiple terminal windows if you decided to work in batches.

Here’s the necessary code ( `download_videos.py` file ):

import json
import urllib
downloader = urllib.URLopener()

# start by getting the full list of videos
with open('items.json') as json_data:    
    items = json.load(json_data)

# where to save the videos (directory)
save_to_dir = './'

def batch(startAt, stopAt):
    index=0

    for item in items:
        index+=1
        if index < startAt or index > stopAt: continue
        print '[%d] Downloading %s..' % (index, item['name'])
        downloader.retrieve(item['url'], '%s%s.mp4' % (
            save_to_dir, item['name']
        ))

# download videos from 1 to 999
batch(1, 999)

You can pass the start number and finish number to the `batch` method if you want to create multiple processes for downloading these videos. After that, just run it with Python again:

python download_videos.py

The videos should start downloading, and the screen should also indicate the current download index, so if you run into any errors you can resume the process at the video index where it stopped.

Download tutorial source code.

Get Alexa Site Rank: Python, PHP and JavaScript

In this quick tutorial we’ll learn about how to use Alexa‘s public API to retrieve the global rank for a given website.

Alexa provides a public API with an XML response containing the queried domain rank data, here’s the API endpoint URI:

http://data.alexa.com/data?cli=10&dat=s&url=example.com

Get Alexa Site Rank with Python:

Python is the best for this, running incredibly fast and on every platform. To get us going, open a new file named `alexa` and place the following code into it:

#!/usr/bin/env python
import urllib, sys, re
xml = urllib.urlopen('http://data.alexa.com/data?cli=10&dat=s&url=%s'%sys.argv[1]).read()
try: rank = int(re.search(r'<POPULARITY[^>]*TEXT="(\d+)"', xml).groups()[0])
except: rank = -1
print 'Your rank for %s is %d!\n' % (sys.argv[1], rank)

and save the file. Next up, make it executable by running

chmod +x alexa

. Now you’re done, and to get a rank run the following:

./alexa google.com

which should output:

Your rank for google.com is 1!

Awesome. You might want to create an alias for this command up next, so you could run

alexa site.tld

easily everywhere in your command line:

nano ~/.bashrc
# towards the bottom, add the following code in a new line:
alias alexa='/path/to/alexa $@'
# save the file (Ctrl+x then y then ENTER)

To have alexa command available, you’ll want to restart the terminal (as it loads the .bashrc file on load) or just reload the .bashrc file with:

. ~/.bashrc

And now, you must have it!

alexa google.com

Get Alexa Site Rank with PHP:

With PHP, it’s quite easier as well and less code could do the job, just as the case with Python. Open a new file named alexa.php and write the following code into it:

#!/usr/bin/env php
<?php

function alexa_rank($site) {
    $xml = file_get_contents("http://data.alexa.com/data?cli=10&dat=s&url={$site}");

    if ( $xml ) {
        preg_match('/<POPULARITY[^>]*TEXT="(\d+)"/si', $xml, $rank);
        return isset($rank[1]) ? (int) $rank[1] : -1;
    }

    return -1;
}

if ( 'cli' == php_sapi_name() ) {
    if ( isset($argv[1]) ) {
        printf ( 'Your rank for %s is %d!%s', $argv[1], alexa_rank($argv[1]), PHP_EOL );
    }
} else {
    // custom usage, also remove the file header line if not running with CLI
    $domain = 'google.com';
    printf ( 'Your rank for %s is %d!%s', $domain, alexa_rank($domain), PHP_EOL );
}

And save the file. You can then use it in the command line:

php alexa.php youtube.com

, `chmod +x` as well if you want to run it without `php` command, or use it in your custom project.

Get Alexa Site Rank with JavaScript:

You can do this in the front-end as well, the only very first challenge you’ll meet is the SAMEORIGIN policy which will prevent you from making XHR requests to the Alexa API. You can get over this by using combining either Python or PHP solutions above to create a server-side API response from your domain and then have access to it, or just use CORS-anywhere project as a proxy.

Here’s the full JS code, which is in native JS which means it could be wrapped in jQuery or any other library:

var domain = 'google.com'
  , req = new XMLHttpRequest()
  , cors = 'https://cors-anywhere.herokuapp.com/';
req.addEventListener('load', function(){
    var r, n;
    r = /<POPULARITY[^>]*TEXT="(\d+)"/g
    n = r.exec(this.response)
    console.log( 'Your rank for '+domain+' is '+( null != n ? n[1] : -1 )+'!' )
});
req.open('GET', cors+'http://data.alexa.com/data?cli=10&dat=s&url='+domain);
req.send();

PHP API Rate Limiting with Redis

Rate Limiting (wiki) is a very useful technique to keep your server resources on a good use, and avoid using your full bandwidth when you are providing free or paid API services that users can be granted access to it. Rate limiting is often used to prevent robots from exhausting your API endpoints, and also as a marketing technique to sell different API access plans with different access counts per given interval (say per minute).

While rate-limiting your app, you’ll want to use less resources possible and handle things incredibly fast to maintain a scaled application, and therefore you should use a no-SQL database as a storing mechanism to keep the identifiers and access counts, or a memory caching technique such as Redis. In this tutorial, I’ll be using Redis because it’s incredibly fast and also easier to implement that any other tool.

Suggested Reading: How To Install and Configure Redis on Ubuntu 16.04

I’ll be using Predis as a PHP Redis client, that’s the only library used in this tutorial so far. You can install it by  either running this command:

composer require predis/predis

or, open composer.json file and save into it the following JSON code:

{
    "require": {
        "predis/predis": "^1.1"
    }
}

and run

composer install

after saving composer.json file.

Next thing, we’ll create an api.php file and require the composer autoload file:

<?php

use Predis\Client as PredisClient;

include __DIR__ . '/vendor/autoload.php';

Now, let’s stop coding for a second and explain the process. We will want to identify our users uniquely in order to serve them the fresh API data, and to apply rate-limiting. Normally you can identify an anonymous user by their IP address, in addition to their browser data namely user-agent attached to the request headers, but maybe you have another unique way of identifying them, like if they are authenticated with a login or an access token, which will be used and it’s even better than using the IP.

Now that you identify your users uniquely, you’ll just want to count their API requests at a given time interval (say each 5 seconds), and limit it to only 2 (or X) requests per that interval. So we’ll use Redis to save and increment these counts, until they reach the limit then we’ll just shout at our users and send them error responses.

Now the cool thing about Redis is the TTL command (time-to-live), so we don’t have to apply additional algorithm to restart our counts each and every 5 seconds. With Redis, set the TTL to 5 at first time:

EXPIRE key 5

and later on, set it to the TTL itself, Redis provides how many seconds left for a key to expire:

TTL key

.

So, we’ll save the counts correctly, check against the user access count to verify whether to serve them a good response or not, and proceed with incrementing the counts for later use.

Here’s the full tutorial code (api.php file):

<?php

use Predis\Client as PredisClient;

include __DIR__ . '/vendor/autoload.php';

function redis() {
    global $Redis;

    if ( !isset($Redis) || !$Redis instanceof PredisClient ) {
        $Redis = new PredisClient;
    }

    return $Redis;
}

function whatismyip() {
    if (isset($_SERVER['HTTP_CLIENT_IP']))
        $ipaddress = $_SERVER['HTTP_CLIENT_IP'];
    else if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
        $ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
    else if(isset($_SERVER['HTTP_X_FORWARDED']))
        $ipaddress = $_SERVER['HTTP_X_FORWARDED'];
    else if(isset($_SERVER['HTTP_FORWARDED_FOR']))
        $ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
    else if(isset($_SERVER['HTTP_FORWARDED']))
        $ipaddress = $_SERVER['HTTP_FORWARDED'];
    else if(isset($_SERVER['REMOTE_ADDR']))
        $ipaddress = $_SERVER['REMOTE_ADDR'];
    else
        $ipaddress = null;
    return $ipaddress;
}

function send_json($resp, $status=200) {
    header('Content-type: application/json; charset=utf-8');

    if ( $status ) { // status code
        http_response_code($status);
    }

    print json_encode($resp);
    die;
}

$limit = [
    'interval'      => 5, // seconds
    'num_requests'  => 2, // number of requests allowed per interval
    'user_ip'       => whatismyip(), // getting the user IP.
];

$uid = "requests_count_{$limit['user_ip']}";
$logged = (int) redis()->get($uid);

if ( !$logged ) {
    // first API request (or the count has expired)
    $logged = 1;
    // log the requests count to 1
    redis()->set($uid, $logged);
    // first time setting the key, expire the key at X seconds 
    redis()->expire($uid, $limit['interval']);
} else if ( $logged + 1 > $limit['num_requests'] ) { // num. requests exceeded
    // send them a notice to slow down
    return send_json([
        'success' => false,
        'message' => 'Too many requests, please slow down or upgrade your API access plan.'
    ], 429);
} else { // we're still below the quota
    // get the time-to-live integer
    $ttl = redis()->ttl($uid);
    // set the key
    redis()->set($uid, $logged+1);
    // expire the key at X seconds (ttl)
    redis()->expire($uid, $ttl);
}

// Send the good response to the good users
send_json(['success' => true, 'data' => 'xyz']);

Don’t forget to send HTTP response code along the API response headers because they’re very useful.

Download the source code here.

How to fix 400 Bad Request – request header or cookie too large

Recently while developing a site for a client that uses cookies, I run into an issue where I had large cookies stored in the browser and sent on every request, while it exceeded the size limit, Apache started throwing 400 (Bad Request) errors on screen.

It is not quite advised that your website stores huge cookies into the browser, even if it is under limit. Each request to your server is made, the cookies are also sent through the request headers and therefore more server resources will be used and it may slow down your site or the request itself.

To go about this 400 error, there are several options..

Storing cookies server-side and using tiny cookies to identify them

The idea is to save the cookies somewhere in your server:

  • In the database
  • In the cache system (Redis is very suitable for this as memory-caching solution)
  • Into temporary files (files that get flushed frequently, such as saving into /tmp/ directory)

After we save a potential cookie, we need to identify it for future so we give it a unique ID. And because we can’t identify a user (unless authenticated) while they’re browsing our site, to serve them their cookies stored server-side, we can also save the cookie IDs into a browser cookie (very small and tiny, nothing to worry about).

You can run a script globally at init that fetches the cookies identifiers from the user cookies, and then loads the cookies values from server-side, and then append these cookies into the $_COOKIE superglobal (so you can use $_COOKIE normally in other code).

Using localStorage (browser local storage API)

Almost every modern browser has support for this feature. Using localStorage you don’t need to worry about the size limitations nor the performance. Chrome for example uses SQLite database system for local storage, which is quite fast. The local storage API is quite simple, you can find a useful reference in the MDN.

One thing about localStorage is it happens on the client-side, so you can’t have access to data stored with it on the server. i.e PHP or Python or whatever server-side language you’re crafting with, won’t have access to these data, unless with some workarounds, like with AJAX.

Process cookies correctly

There are several mistakes we do while we store a cookie. One can have large amount of data saved into a cookie while only small bytes of raw text can be saved. Shorten the cookie values the most possible, and you saved yourself more cookies. Care to share?

Another important mistake we do, which also solved my case, is we save cookies globally across the domain. Most cookies we save are intended to be used only at specific route or web directory, not in the global root (which loads them across all subdirectories or routes) which makes it quite a mess and overloads the requests. Whatever language you’re using, it has native support for cookie processing and you should specify the static path of the cookie as the exact path where you want it to be loaded.

In general, browser cookies are meant to store small bytes of raw text that could be used to identify users, tracking, etc and not a client-side database.

Find broken links in a project files with Python

In this quick tip, we will learn about scanning a project directory HTML files for quickly fetching links and scan for broken links that we have. Broken links are usually a bad practice and something that one worried about their SEO score should detect and remove or update, as they affect your ranking in a bad way.

In this tutorial, we’re going to use pyquery to parse the files into HTML, and query for links the same way we do in JQuery.

To get pyquery, you can install it via Python package manager pip:

pip install pyquery

We will also use urllib2 to fetch a link and find out the response status code whether it is a normal response or the opposite. for this purpose, we’re looking into 404 HTTP error code, which stands for a not found web page.

Here’s the code:

# -*- coding: utf-8 -*-
from pyquery import PyQuery as pq
import sys
import urllib2
import glob
import os
import fnmatch
import re

status_code = None

def is404(url):
    global status_code
    req = urllib2.Request(url)
    try:
        resp = urllib2.urlopen(req)
    except urllib2.HTTPError as e:
        status_code = e.code
        return e.code == 404
    except urllib2.URLError as e:
        return None
    else:
        return False

try:
    ext = sys.argv[1]
except:
    ext = None

if not ext:
    print 'You must provide a file extension (e.g html, php)!'
    sys.exit(0)

items = []

for root, dirs, files in os.walk('.'):
    for basename in files:
        if fnmatch.fnmatch(basename, '*.%s'%ext):
            items.append(os.path.join(root, basename))

for item in items:
    with open(item) as c:
        raw = c.read()
        q = pq(raw)

        for a in q('a'):
            try:
                href = a.attrib['href']
            except:
                continue

            if not re.match('(https?)?:\/\/', href):
                # invalid link, internal link probably
                continue

            if is404(href):
                print '%s is a broken link! Status Code %s (located in %s)' % (href, status_code, item)

You should probably save that code into a new file named scan.py or whatever, and execute it inside the directory where your project files are. The file extension is HTML but you can also scan other file extensions such as .php or others, here’s a simple usage:

I have a directory where a sample project is. Here’s a quick view.

(ct) samuel@samuel-dell:~/www/python/ct/broken$ ls -R
.:
about  index.html  scan.py

./about:
index.html

Now I can just run

python scan.py html

and it will recursively search all HTML files within this directory and search for HTML anchors, extract their HREF attributes, validate it, and then check if it is not a broken link.

(ct) samuel@samuel-dell:~/www/python/ct/broken$ python scan.py html
https://www.google.com/cats is a broken link! Status Code 404 (located in ./about/index.html)

It will tell where the broken link is (file location) so you could jump into there to fix it. It may also take some long time to process and it all depends on how many links you have, and your connectivity may also play a role in this.

Integrate credit card payments with Stripe

In this quick tip tutorial we will learn about using Stripe API to receive payments from credit cards and charging users and verifying the payments.

Getting Started

We’ll create a folder for our project and use it. For the sake of this tutorial, I’m naming it to stripe-payments.

Right after that, we’ll need to require some dependencies. We’ll need the latest PHP SDK for Stripe from their Github repository https://github.com/stripe/stripe-php and load it.

  • If you are using composer:

To autoload this library with composer just run the following throw the CLI (considering you’re in stripe-payments directory):

composer require stripe/stripe-php
  • If you don’t use composer:

You know you’re missing out if you don’t use composer yet, but you can download a release from Github for Stripe PHP SDK and load it in your project:

Go to Stripe PHP releases, click the latest tag (at this moment v4.9.1), and download code. Unpack it to the project folder which in my case is stripe-payments.

Loading Stripe library and using it

We’ll keep things simple enough and use 2 static PHP files to do the job:

  • index.php where we will place the payment button from Stripe checkout
  • charge.php which will be used as the form processor to verify the front-end submitted data and make actual charges.

So, go ahead and create these 2 files. Next we’ll load Stripe library into index.php file:

  • If you use composer:

index.php

<?php

require __DIR__ . '/vendor/autoload.php';
  • If you don’t use composer:

index.php

<?php

require __DIR__ . '/stripe-php-4.9.1/init.php';

Just make sure you got the directory name right, you might be having a different release than “stripe-php-4.9.1”.

Great, now if we navigate to your project in the browser http://127.0.0.1/stripe-payments/ you should probably see a blank screen. If so, then you’re at it, otherwise if you get a 500 error page or something then make sure you have the correct files required. Debugging is always necessary so let’s add some code to the top of our index.php and charge.php files ( this is totally optional and you should not leave debugging mode on when in production):

<?php

error_reporting(E_ALL);
ini_set('display_errors', 1);

# ... reset of code

Now if you have any errors they’ll be printed wide and clear in the browser, in order for you to debug them properly.

Basic configuration

Let’s now place some constants into the top of our index.php in order to configure the process properly.

  • STRIPE_PUBLIC_KEY: Your API public key which can be obtained from the dashboard dashboard.stripe.com
  • STRIPE_SECRET_KEY: Your API secret, can be obtained from the same dashboard
  • STRIPE_PRICE: The price we are charging here. It should be an integer and in cents (pennies) so multiply it by 100 ($50 becomes 5000 and £0.5 becomes 50)
  • CURRENCY_CODE: The ISO code for the currency you are charging for. USD for the US dollar $, EUR for euro €, GBP for £ the British pound, you can find a list of currency codes and their symbols online.
  • USER_EMAIL: Optional, if you already have a user signed in or you only allow logged in users to make payments, then place here the current user’s email address otherwise the user will be free to choose an email address in order to make a payment.

So we’ll add them as constants into our index.php file (from now on, index.php will handle every request, even calling charge.php when form data has been passed):

index.php


<?php

error_reporting(E_ALL);
ini_set('display_errors', 1);

require __DIR__ . '/vendor/autoload.php';

// API public key
define ( 'STRIPE_PUBLIC_KEY', 'pk_test_6pRNASCoBOKtIshFeQd4XMUh' );

// API secret key
define ( 'STRIPE_SECRET_KEY', 'sk_test_BQokikJOvBiI2HlWgH4olfQ2' );

// stripe amount
define ( 'STRIPE_PRICE', 100 ); // that's $1

// amount currency
define ( 'CURRENCY_CODE', 'USD' );

// current user email
define ( 'USER_EMAIL', 'me@myself.ie' );

Using Stripe Checkout tool

What’s amazing about Stripe is it saves you all the trouble of coding the payment forms, validating credit card numbers and form fields, and everything! Stripe Checkout gives you the opportunity to place a simple button which the user can click to get a neat popup where they fill in their card information to make the payment. Once the information have been submitted successfully, Stripe Checkout will submit then the form to your server (charge.php in this tutorial) and from there you’ll use Stripe API to make actual payments (because no payment has been made yet).

We can then with no further due add a form with their checkout JS to our index page and actually start displaying content!

index.php


<?php

error_reporting(E_ALL);
ini_set('display_errors', 1);

require __DIR__ . '/vendor/autoload.php';

// API public key
define ( 'STRIPE_PUBLIC_KEY', 'pk_test_6pRNASCoBOKtIshFeQd4XMUh' );

// API secret key
define ( 'STRIPE_SECRET_KEY', 'sk_test_BQokikJOvBiI2HlWgH4olfQ2' );

// stripe amount
define ( 'STRIPE_PRICE', 100 ); // that's $1

// amount currency
define ( 'CURRENCY_CODE', 'USD' );

// current user email
define ( 'USER_EMAIL', 'me@myself.ie' );
// start output
?>

<!DOCTYPE html>
<html>
<head>
 <title>My Amazing Membership Site</title>
</head>
<body style="background: #ececec; display: table; margin: 0 auto; padding-top: 5vw">

<form action="index.php?charge=1" method="POST">
 <h3>Pay membership with credit/debit card</h3>

 <p>This is a one-time payment. You will be prompted to enter your card details securly.</p>

 <script
 src="https://checkout.stripe.com/checkout.js" class="stripe-button"
 data-key="<?php echo STRIPE_PUBLIC_KEY; ?>"
 data-email="<?php echo USER_EMAIL; ?>"
 data-amount="<?php echo STRIPE_PRICE; ?>"
 data-name="My Amazing Site"
 data-description="Premium Membership / For Life!"
 data-image="http://lorempixel.com/150/100/cats/"
 data-currency="<?php echo CURRENCY_CODE; ?>"
 data-locale="auto">
 </script>
</form>

</body>
</html>

Great so now we have some content in our index!

Go ahead and click the pay button, you’ll see the popup containing the credit card credentials fields, and the icon, title, description and button text along with other generic data can be customized in the script tag within the form, for instance loose the cat and add a real icon image to your card by specifying the image URL through data-image

data-image="http://lorempixel.com/150/100/cats/"

Cool! To do some test payment, Stripe has some sandbox card which number is 4242 4242 4242 4242, the expiration can be any future date in format MM/YY (10/19 for October 2019), and CVC can be a random 3 digits number so 123.

The remember button will basically let Stripe remember the card details for the current site and fill them automatically upon the next request securly, this is not something we’d be worried about for the moment, nor collecting the card details as well, we don’t need them.

We’re not going to submit the card yet, let’s make sure charge.php file is involved when doing so, so as to verify payments:

index.php


<?php

error_reporting(E_ALL);
ini_set('display_errors', 1);

require __DIR__ . '/vendor/autoload.php';

// API public key
define ( 'STRIPE_PUBLIC_KEY', 'pk_test_6pRNASCoBOKtIshFeQd4XMUh' );

// API secret key
define ( 'STRIPE_SECRET_KEY', 'sk_test_BQokikJOvBiI2HlWgH4olfQ2' );

// stripe amount
define ( 'STRIPE_PRICE', 100 ); // that's $1

// amount currency
define ( 'CURRENCY_CODE', 'USD' );

// current user email
define ( 'USER_EMAIL', 'me@myself.ie' );

// if the form was submitted (i.e payment request sent)
if ( isset($_GET['charge']) && $_POST ) {
require __DIR__ . '/charge.php';
}

// start output
?>

<!DOCTYPE html>
<html>
<head>
<title>My Amazing Membership Site</title>
</head>
<body style="background: #ececec; display: table; margin: 0 auto; padding-top: 5vw">

<form action="index.php?charge=1" method="POST">
<h3>Pay membership with credit/debit card</h3>

<p>This is a one-time payment. You will be prompted to enter your card details securly.</p>

<script
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
data-key="<?php echo STRIPE_PUBLIC_KEY; ?>"
data-email="<?php echo USER_EMAIL; ?>"
data-amount="<?php echo STRIPE_PRICE; ?>"
data-name="My Amazing Site"
data-description="Premium Membership / For Life!"
data-image="http://lorempixel.com/150/100/cats/"
data-currency="<?php echo CURRENCY_CODE; ?>"
data-locale="auto">
</script>
</form>

</body>
</html>

Now lets get into charge.php file, we actually want to debug the data passed by checkout.js to our server:

charge.php


<?php

// prevent direct access, only index.php can include this file
defined ( 'STRIPE_SECRET_KEY' ) || exit ( 'Direct access not allowed' . PHP_EOL );

use \Stripe\Stripe;
use \Stripe\Customer;
use \Stripe\Charge;

Stripe::setApiKey(STRIPE_SECRET_KEY);

$token = isset($_POST['stripeToken']) ? $_POST['stripeToken'] : null;

if ( !$token ) {
return; // just in case
}

echo var_dump($_POST);
exit;

Registering the customer and charging them

So far so good. Now if you submit the payment for with dummy details:

  • Card number: 4242 4242 4242 4242
  • Expiration: (some future month)/(some future year): 10/19
  • CVC: 123

We’ll now have checkout.js submit the form automatically and we will see an array of data passed, among them stripeToken which will be the token Stripe PHP SDK requires to create the customer and make the payment:

$customer = Customer::create(array(
'email' => USER_EMAIL,
'source' => $token
));

Now usually this is the step before las. It can either be successful creating a customer or throw a warning of some exception, so we’ll wrap it in try/catch block to make sure we handle the request properly.

A one last step is charging this customer using their id returned:

$charge = Charge::create(array(
'customer' => $customer->id,
'amount' => STRIPE_PRICE,
'currency' => CURRENCY_CODE
));

Now usually this should throw errors when something is not well, so we will again use the try/catch bloc and not worry about the different exceptions that Stripe throws because in the end all of them are pointing to the fact that an error occured and no payment has been made.

In the catch bloc you might want to log the request, send you an email as the site admin, and tell the user to hang in there until you inspect the issue. This is rare to happen unless of course we have a mis-configured something  or the user uses the same form data many times etc. (The code comes after)

We can then charge the user and show them custom success notices, but we are also supposed to be doing this with care and make sure their membership is upgraded as paid or some other action is taken, a good thing worth mentioning is sending them a custom notice to their inbox to let them know and keep track of this transaction.

And of course, if you visit your Stripe dashboard you’ll notice you have made some earnings in the transactions graph. I have $2 in the screenshot because I actually forgot to take one until I made already 2 transactions for this tutorial.

Wrapping up

To wrap up, here’s the final code for both files index.php and charge.php

index.php


<?php

error_reporting(E_ALL);
ini_set('display_errors', 1);

require __DIR__ . '/vendor/autoload.php';

// API public key
define ( 'STRIPE_PUBLIC_KEY', 'pk_test_6pRNASCoBOKtIshFeQd4XMUh' );

// API secret key
define ( 'STRIPE_SECRET_KEY', 'sk_test_BQokikJOvBiI2HlWgH4olfQ2' );

// stripe amount
define ( 'STRIPE_PRICE', 100 ); // that's $1

// amount currency
define ( 'CURRENCY_CODE', 'USD' );

// current user email
define ( 'USER_EMAIL', 'me@myself.ie' );

$success = $error = array();

// if the form was submitted (i.e payment request sent)
if ( isset($_GET['charge']) && $_POST ) {
require __DIR__ . '/charge.php';
}

// start output
?>

<!DOCTYPE html>
<html>
<head>
<title>My Amazing Membership Site</title>
</head>
<body style="background: #ececec; display: table; margin: 0 auto; padding-top: 5vw">

<form action="index.php?charge=1" method="POST">
<?php if ( $success ) : ?>
<ul style="background: #d3f1d3; display: block; padding: 1em; border: 1px solid #ddd; border-radius: 3px;">
<li><?php echo implode ( '</li><li>', $success ); ?></li>
</ul>
<?php endif; ?>

<?php if ( $error ) : ?>
<ul style="background: #fec8b7; display: block; padding: 1em; border: 1px solid #ddd; border-radius: 3px;">
<li><?php echo implode ( '</li><li>', $error ); ?></li>
</ul>
<?php endif; ?>

<h3>Pay membership with credit/debit card</h3>

<p>This is a one-time payment. You will be prompted to enter your card details securly.</p>

<script
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
data-key="<?php echo STRIPE_PUBLIC_KEY; ?>"
data-email="<?php echo USER_EMAIL; ?>"
data-amount="<?php echo STRIPE_PRICE; ?>"
data-name="My Amazing Site"
data-description="Premium Membership / For Life!"
data-image="http://lorempixel.com/150/100/cats/"
data-currency="<?php echo CURRENCY_CODE; ?>"
data-locale="auto">
</script>
</form>

</body>
</html>

charge.php


<?php

// prevent direct access, only index.php can include this file
defined ( 'STRIPE_SECRET_KEY' ) || exit ( 'Direct access not allowed' . PHP_EOL );

use \Stripe\Stripe;
use \Stripe\Customer;
use \Stripe\Charge;

Stripe::setApiKey(STRIPE_SECRET_KEY);

$token = isset($_POST['stripeToken']) ? $_POST['stripeToken'] : null;
unset($_POST);

if ( !$token ) {
return; // just in case
}

try {
$customer = Customer::create(array(
'email' => USER_EMAIL,
'source' => $token
));
} catch ( \Exception $e ) {
$error []= "Error registering your payment request. Please try again or later!";

return;
}

try {
$charge = Charge::create(array(
'customer' => $customer->id,
'amount' => STRIPE_PRICE,
'currency' => CURRENCY_CODE
));
} catch ( Exception $e ) {
error_log( sprintf('ERROR: Stripe failed for user %s @%d (ip:%s)', USER_EMAIL, time(), $_SERVER['REMOTE_ADDR']) );
error_log( print_r( $e, true ) );

$error []= "Error verifying your payment request. Please try again or later if you are sure no payment has been made yet";
return;
}

// Now we're dealing with a verified payment! Let's upgrade the user!

// a success notice
$success []= "Your payment has been verified successfully! We're upgrading your membership.";

if ( function_exists('upgrade_user') ) {
$upgraded = upgrade_user(USER_EMAIL);

if ( !$upgraded ) {
$error []= "We could not upgrade your membership. Please sit tight as we do it manually";

error_log( sprintf('Upgrade user %s failed, do it manually ASAP!!', USER_EMAIL) );
}
}

You can download the source code from my gist https://gist.github.com/elhardoum/8cffce0c62f7c554ca0d57247e78fff2

Going Live

Before going live, in addition to disabling debug mode (removing the error_reporting and ini_set to the top of index.php file), you are required to have a valid SSL certificate on your site. It makes sense to enable SSL when you have a user authentication site for security, now it makes even more sense to secure your payments requests.

If you are on shared hosting and cannot afford an SSL, you can always use CloudFlare’s free SSL which is included along with the other free services they have in their free plan. Otherwise if you can afford one, I recommend Godaddy or NameCheap (that’s my referral link).

If you already have a dedicated server or a VPS then problem solved with LetsEncrypt’s powerful and free SSL certificates.

You also switch the API keys from sandbox (test keys) to live keys, as long as you activated your account. Here’s a useful checklist about going live with Stripe.

Create MySQL database and user without PHPMyAdmin

In this quick tip tutorial, we’ll learn about the following:

  • How to create a MySQL database
  • How to create a user that connects to this database
  • How to do all of the above from the terminal without PHPMyAdmin

Now if you were installing WordPress or a PHP application whatsoever for your site, you’ll normally be opted to provide credentials for a database and MySQL user for these apps to make database connections. FYI, I am running a light stack of LAMP on Ubuntu 16.04 which is my preferred local development environment.

Before you get this started, open in your terminal and init MySQL terminal typing the following:

mysql -u root -p

The above code will prompt you to enter your mysql password, which you have provided while configuring your mysql server. There is another way to do this without being prompted to enter the password, while you’re on root, using the -e flag followed by the MySQL query, something like:

mysql -u root -e "SHOW DATABASES;"

and hopefully it wouldn’t request for a password.

Step 1 – Create database

You just want to type in the name of your database to the following command:

CREATE DATABASE my_database_name;

And pressing enter will create your new database named my_database_name. Let’s make sure it is there:

SHOW DATABASE;

You’ll see a tree of databases which you have in your install, among them the one we just created.

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| bbpm |
| mu2 |
| my_database_name |
| mysql |
| performance_schema |
| sys |
| wp |
+--------------------+
8 rows in set (0.06 sec)

mysql> 

 

 

Great for step 1, the database is there.

Step 2 – Create a MySQL user and grant permissions

Good, so to usually connect to this database we have to create a mysql user and grant them permissions. Let’s begin by creating the user:

CREATE USER 'myuser'@'localhost' IDENTIFIED BY 'mypassword';
  • username: The MySQL user username (myuser in this example).
  • host: To enable this user for remote connections, user ‘%’ instead of ‘localhost’ in the host field
  • password: The password which the user will need to enter in order to be granted a connection. Leave as empty ” if you are on a local development server. (the password is set to ‘mypassword’ in this example)

Replace the text in orange with the user name of your choice, and the password as well. Remember, the password will be used to connect to this database using this user.

That will create the user, but yet we want to grant them privileges to perform SQL queries:

GRANT ALL PRIVILEGES ON * . * TO 'myuser'@'localhost';

This should give the user full privileges across the MySQL server (i.e all databases). To give permissions in a single database or two, use:

GRANT ALL PRIVILEGES ON databasename.* TO 'myuser'@'localhost';

The 2nd wildcard (star) may be used as well to reflect a data table, so in case the privileges should be restricted to a database table you can use it.

Lastly, we want to flush the privileges:

FLUSH PRIVILEGES;

Great, so all done for step 2, but before that, let’s test this new user and database out to find out!

Step 3 – Just a test

So as we created the new user, we want to see whether everything’s functional. Remember the username and password you stated above, let’s use them to connect:

First, ctrl+d or type in ‘bye’ to disconnect from the mysql session we used with root, in the beginning of this tutorial. Now, let’s connect with out new user:

mysql -u myuser -p

You’ll be prompted to enter a password, the one you provided in step 2. Connected? Cool! then everything’s normal.

mysql> ^DBye
samuel@Samuel:~/htdocs$ mysql -u myuser -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 5.7.17-0ubuntu0.16.04.1 (Ubuntu)

Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| bbpm               |
| mu2                |
| my_database_name   |
| mysql              |
| performance_schema |
| sys                |
| wp                 |
+--------------------+
8 rows in set (0.00 sec)

mysql> use my_database_name;
Database changed
mysql> show tables;
Empty set (0.00 sec)

mysql> 

We now have the database empty with no tables and that is our main point, the apps will opt to ask for a clean database and they append their database tables upon a fresh install, like WordPress, for instance.

You could manually create database tables as well, first just make sure you switched to your target database:

USE my_database_name;

It is always good to follow the manual, here are few resources:

Add WordPress network settings page for your plugin

In this tutorial, we will learn how to add a network settings page for your WordPress theme or plugin, add settings fields to this page, and update these fields.

Add network settings page – register menu

If you haven’t already added settings page for your WordPress theme or plugin in a regular non-multisite install, there are different hooks you can use but the proper one is admin_menu, now for a network you’ll notice that WordPress prefixes most of these hooks with network_ in the beginning and so we will be after network_admin_menu hook to register our page with add_submenu_page function.

Since the title says “settings”, we will be adding our page to settings menu, which has file of settings.php, but you can always add to different parent menus such as sites.php, tools.php, etc. Here’s some class to get us started:

<?php namespace MyPlugin\Admin;

class Admin
{

    /**
      * This method will be used to register
      * our custom settings admin page
      */

    public function init()
    {
        add_action('network_admin_menu', array($this, 'setupTabs'));
    }

    /**
      * This method will be used to register
      * our custom settings admin page
      */

    public function setupTabs()
    {
        \add_submenu_page(
            'settings.php',
            'My Plugin Settings',
            'My Plugin',
            'manage_options',
            'my-plugin',
            array($this, 'screen')
        );

        return $this;
    }

    /**
      * This method will parse the contents of
      * our custom settings age
      */

    public function screen()
    {
        printf('Hello from %s::%s!', __CLASS__, __FUNCTION__);
    }
}

$MyPluginAdmin = new \MyPlugin\Admin\Admin;
$MyPluginAdmin->init();

Great! Now if you properly place the above class declaration code into a file and call it, it will register us the custom network admin settings page, which will print out some basic dummy text.

Add network settings page – parse content

From this step we will be after parsing the settings contents and fields to our custom settings page. Now as if you don’t know there is a handy tool for registering and processing custom settings in the WordPress plugin API, but I’ll do this tutorial in the boring way which will consist of basically 1 or 2 fields processed with native PHP and of course WordPress API.

Now that you’re determined to put together couple settings for your admin settings page, it should make sense that these settings should be global (the same on all over the network) since the plugin is network-wide active, unlike when it is active for a given blog where you probably want the settings to be saved just to that blog instance. Therefore, we will be using update_site_option and get_site_option to process/get our settings, which will save the settings to the network meta and therefore they will be global.

Add network settings page – wrap up

Follows the wrap-up of our basic class code used to make 2 example fields of settings, update them and get their settings.

<?php namespace MyPlugin\Admin;

class Admin
{
    public $updated;
    /**
      * This method will be used to register
      * our custom settings admin page
      */

    public function init()
    {
        // register page
        add_action('network_admin_menu', array($this, 'setupTabs'));
        // update settings
        add_action('network_admin_menu', array($this, 'update'));
    }

    /**
      * This method will be used to register
      * our custom settings admin page
      */

    public function setupTabs()
    {
        \add_submenu_page(
            'settings.php',
            __('My Plugin Settings', 'my-plugin-domain'),
            __('My Plugin'),
            'manage_options',
            'my-plugin',
            array($this, 'screen')
        );

        return $this;
    }

    /**
      * This method will parse the contents of
      * our custom settings age
      */

    public function screen()
    {
        ?>

        <div class="wrap">

            <h2><?php _e('My Plugin Admin', 'my-plugin-domain'); ?></h2>

            <?php if ( $this->updated ) : ?>
                <div class="updated notice is-dismissible">
                    <p><?php _e('Settings updated successfully!', 'my-plugin-domain'); ?></p>
                </div>
            <?php endif; ?>

            <form method="post">

                <p>
                    <label>
                        <?php _e('Enter your email address below:', 'my-plugin-domain'); ?>
                        <br/>
                        <input type="email" name="email" value="<?php echo esc_attr($this->getSettings('email')); ?>" size="50" />
                    </label>
                </p>

                <p>
                    <label>
                        <?php _e('Select your age range:', 'my-plugin-domain'); ?>
                        <br/>
                        <select name="age_range">
                            <option value="" <?php selected($this->getSettings('age_range'), null); ?>><?php _e('Select Range', 'my-plugin-domain'); ?></option>
                            <?php foreach ( array('13-18', '18-26', '26-40', '40-60') as $range ) : ?>
                                <option value="<?php echo esc_attr($range); ?>" <?php selected($this->getSettings('age_range'), $range); ?>><?php echo esc_attr($range); ?></option>
                            <?php endforeach; ?>
                        </select>
                    </label>
                </p>

                <?php wp_nonce_field('my_plugin_nonce', 'my_plugin_nonce'); ?>
                <?php submit_button(); ?>

            </form>

        </div>

        <?php
    }

    /**
      * Check for POST (form submission)
      * Verifies nonce first then calls
      * updateSettings method to update.
      */

    public function update()
    {
        if ( isset($_POST['submit']) ) {
            
            // verify authentication (nonce)
            if ( !isset( $_POST['my_plugin_nonce'] ) )
                return;

            // verify authentication (nonce)
            if ( !wp_verify_nonce($_POST['my_plugin_nonce'], 'my_plugin_nonce') )
                return;

            return $this->updateSettings();
        }
    }

    /**
      * Updates settings
      */

    public function updateSettings()
    {
        $settings = array();

        if ( isset($_POST['email']) && is_email($_POST['email']) ) {
            $settings['email'] = esc_attr($_POST['email']);
        }

        if ( isset($_POST['age_range']) && trim($_POST['age_range']) ) {
            $settings['age_range'] = sanitize_text_field($_POST['age_range']);
        }

        if ( $settings ) {
            // update new settings
            update_site_option('my_plugin_settings', $settings);
        } else {
            // empty settings, revert back to default
            delete_site_option('my_plugin_settings');
        }

        $this->updated = true;
    }

    /**
      * Updates settings
      *
      * @param $setting string optional setting name
      */

    public function getSettings($setting='')
    {
        global $my_plugin_settings;

        if ( isset($my_plugin_settings) ) {
            if ( $setting ) {
                return isset($my_plugin_settings[$setting]) ? $my_plugin_settings[$setting] : null;
            }
            return $my_plugin_settings;
        }

        $my_plugin_settings = wp_parse_args(get_site_option('my_plugin_settings'), array(
            'email' => null,
            'age_range' => null
        ));

        if ( $setting ) {
            return isset($my_plugin_settings[$setting]) ? $my_plugin_settings[$setting] : null;
        }
        return $my_plugin_settings;
    }
}

$MyPluginAdmin = new \MyPlugin\Admin\Admin;
$MyPluginAdmin->init();

Here’s a screenshot also:

Add WordPress network settings page for your plugin

« Older posts

© 2017 Samuel Elh - Powered by WordPress, DigitalOcean & NameCheap

Theme by Anders NorenUp ↑

Subscribe to our mailing list

Sign up to receive updates about WordPress, free and premium plugins and themes in general and tips and tricks

* indicates required