Samuel Elh Blog

WordPress, bbPress, BuddyPress, JavaScript tutorials and snippets

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 uses SQLite database system for local storage. The local storage API is quite simple, you can find a useful reference in the MDN.

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.

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';

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';

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

Gist: List active plugins in WordPress (including multisite)

This gist is intended to help you list all of the active plugins in your WordPress installation, involving network active plugins when in a multisite.

The Gist:

function get_active_plugins_list() {
	// get blog active plugins
	$plugins = apply_filters('active_plugins', get_option('active_plugins'));

	if ( is_multisite() ) {
		// get active plugins for the network
	    $network_plugins = get_site_option('active_sitewide_plugins');
	    if ( $network_plugins ) {
	        $network_plugins = array_keys($network_plugins);
	        $plugins = array_merge($plugins, $network_plugins);
	    }
	}

	return $plugins;
}

Usage:

Check if a plugin is active:

$my_plugin = 'bbpress/bbpress.php';

if ( in_array($my_plugin, get_active_plugins_list()) ) {
    // bbPress is active
} else {
    // bbPress is not active
}

Print the plugins list:

print '<ul>';
print '<li>';
print implode('</li><li>', get_active_plugins_list());
print '</li>';
print '</ul>';

Add custom fields to bbpress profile

bbPress is great free and open source plugin for the implementation of forums and user profiles and discussion on a WordPress blog/website or a multisite. It comes with great features out of the box, using WordPress API to build neat and SEO-friendly forums, with many other features.

The many advantages of bbPress is, it is easy to use, and fully extensible for the sake of bringing more features and tools. Today, we’ll go through some easy steps to add additional custom fields to bbPress profile, and displaying them on the user profile, as well as add fields for those in the bbPress profile edit, and handle processing everything.

Add custom fields to bbPress profile edit form

There are so many HTML5 field types you can add as custom fields, from checkbox to Google Maps coordinates, but for the sake of simplicity we’ll keep it simple as possible and go for an example of a simple drop down menu for letting users to select their own cities.

Finding the perfect section in profile edit to parse custom fields:

If you take a look at templates/default/bbpress/form-user-edit.php file located in the bbPress plugin folder, that is actually the code that makes up the profile edit page template. You’ll notice many do_action callback lines, that is the WordPress API used to let us extend the area and add other fields. The point is, I’ll be hooking to a specific hook, but you should always look at that file to find the appropriate hook name to use for you case, so as to parse fields in the appropriate section possible.

Here’s a hook list that might be at your help (it is not a complete one, I focused on the hooks occurring after each section fields since we’re after adding extra custom fields):

  • bbp_user_edit_after_name: Will add fields after the “Name” section in bbPress edit profile
  • bbp_user_edit_after_contact: Will parse fields after “Contact Info” section fields.
  • bbp_user_edit_after_about: Will add fields after “About Yourself” section.
  • bbp_user_edit_after_account: Will add fields after “Account” section.
  •  bbp_user_edit_after_role: Will add fields “User Role” section fields.

You may notice all the hooks point to “after” in the name, there’s also “before” hooks to parse fields before the existing ones, and you can use them as well, I just felt lazy letting them in!

You can and may also register a custom section for your data, either before everything (bbp_user_edit_before) or after (bbp_user_edit_after), and use the same HTML section containers as bbPress to get the matching style, or style your custom section accordingly.

Parse custom field into bbPress profile edit:

Once you have the appropriate action hook, hook it to function callback that prints our custom fields and embeds them. Like I mentioned earlier, I am going to add a drop-down city select for users, so the best section for the case is under “Contact Info”:

add_action( "bbp_user_edit_after_contact", "se_add_city_field" );

function se_add_city_field() {
    // a random selection of cities
    $cities = array( "Seoul", "Mexico City", "Amsterdam", "Mumbai", "Agadir", "Egypt" );
    ?>
    <div>
        <label for="city"><?php _e( 'City', 'my-domain' ); ?></label>
        <select name="city" id="city">
            <option value="">Select City</option>
            <?php foreach ( $cities as $city ) : ?>
                <option><?php echo esc_attr( $city ); ?></option>
            <?php endforeach; ?>
        </select>
    </div>
    <?php
}

Now you’ll probably notice there’s an extra field for city that has just been appended to bbPress profile edit form, below contact info fields:

city field added bbpress

That’s great. Now we are done with the step of appending the field to the edit profile form. Guess next up is processing this field upon user saving their profile and save the right data to the database? carry on.

Process custom fields while saving bbPress profile

This is quite a simple step as bbPress provides hooks to hook into profile saving.

We’ll check if the city field is set and if the city is selected, then save this city into a custom user meta (in the database), otherwise, delete the user meta for city to let the user unset their city by anytime:

// when updating our profile
add_action( "personal_options_update", "se_save_city_value" );
// when updating other users' profiles
add_action( "edit_user_profile_update", "se_save_city_value" );

function se_save_city_value( $user_id ) {
    // exclude profile.php/user-edit update
    if ( is_admin() ) return;
    // update preference
    if ( isset( $_POST['city'] ) && $_POST['city'] ) {
        return update_user_meta( $user_id, "se_city", sanitize_text_field( $_POST['city'] ) );
    } else {
        return delete_user_meta( $user_id, "se_city" );
    }
}

I hooked into two action hooks this time because I noticed bbPress fires the first one only when a user is updating their own profile, while the second one runs when updating others profiles, as any admin can edit other users which makes sense.

Now running the first test, I am selecting a city and saving profile, the city has been saved in a custom user meta with key se_city which we can use later to embed the selected city into user profile, and also to check the selected city in the profile edit screen in the drop-down menu for city:

function se_add_city_field() {
    // a random selection of cities
    $cities = array( "Seoul", "Mexico City", "Amsterdam", "Mumbai", "Agadir", "Egypt" );
    // selected city, if any
    $user_city = bbp_get_displayed_user_field('se_city');
    ?>
    <div>
        <label for="city"><?php _e( 'City', 'my-domain' ); ?></label>
        <select name="city" id="city">
            <option value="">Select City</option>
            <?php foreach ( $cities as $city ) : ?>
                <option <?php selected($user_city,$city); ?>><?php echo esc_attr( $city ); ?></option>
            <?php endforeach; ?>
        </select>
    </div>
    <?php
}

I went back to edit the function se_add_city_field() which embeds the country drop-down in the profile edit screen. To get any kind of user meta while in the bbPress profile, use

bbp_get_displayed_user_field( $meta_key )

where $meta_key is the custom user meta key, let it be ID to get the displayed user ID, or se_city to get the city we just saved (in the database, not in the real world).

I also used selected() to auto select the option that corresponds to the selected city in the drop-down. You can use checked() when dealing with radio buttons or check-boxes, or PHP to add attributes and HTML properly. It is always a neat idea to use the helper functions provided by WordPress for better performance, security and all.

Display custom fields in bbPress user profile

After all, we can say we have added custom fields to bbPress profiles and users can now edit their profiles to input their correct details. We can now embed these custom fields anywhere, let it be in their bbPress profile, in a custom bbPress profile tab, below topic or reply author details for this user, etc, etc.

For the sake of simplicity for this tutorial I am going to parse the city field in the bbPress user profile, and in the topic/reply after author details.

After bbPress user profile:

// add city after user profile details
add_action( "bbp_template_after_user_profile", "se_embed_city_profile" );

function se_embed_city_profile() {
    // selected city, if any
    $user_city = bbp_get_displayed_user_field('se_city');
    // making sure user has set their city first
    if ( !trim( $user_city ) ) return;
    // all good
    return printf(
        '<p>%s is based on <strong>%s</strong></p>',
        esc_attr( bbp_get_displayed_user_field('nickname') ),
        esc_attr( $user_city )
    );
}

I am using the bbPress function bbp_get_displayed_user_field() to get the nickname and all, because it already loads all this users’s meta which makes sense for great performance, and I am taking advantage of that, while you can use other methods to get the displayed user data, such as ID with bbp_get_displayed_user_id(), display name with

get_userdata( bbp_get_displayed_user_id() )->display_name

, etc..

Now here’s a preview of the city field displayed into my bbPress profile on my local installation:

city field bbpress profile

Great! We promised to embed the city below bbPress topics and replies as well, right after the author details, so here we go:

// add city after topic/reply author details
add_action( "bbp_theme_after_reply_author_details", "se_embed_city_forums" );

function se_embed_city_forums() {
    // topic/reply author
    $user_id = bbp_get_reply_author_id();
    // selected city, if any
    $user_city = get_user_meta( $user_id, 'se_city', true );
    // making sure user has set their city first
    if ( !trim( $user_city ) ) return;
    // all good
    return printf(
        '<p>%s is based on <strong>%s</strong></p>',
        esc_attr( bbp_get_displayed_user_field('nickname') ),
        esc_attr( $user_city )
    );
}

This time we use

bbp_get_reply_author_id()

function to get the correct user ID for the topic/reply author, and

get_user_meta()

to get the custom meta saved as city.

Here’s a preview:

city field bbpress topic-reply

Full Tutorial Code

Fin

That is it for this tutorial. I hope that helps. Feel free to discuss below. Thank You!

Using version_compare to run WordPress plugin on minimal PHP requirements

As we know, WordPress can run in an environment with very minimal technical requirements, including an outdated PHP version of 5.2, which was EOL’d couple years ago, and can run as well with the latest PHP software.

As WordPress plugin/theme developers, we get support messages and notifications frequently from users encountering errors and bugs while trying to run our plugin or theme while the minimal technical requirements aren’t met. It is not their fault that they don’t upgrade their PHP software to the min required, but it is actually our fault, could be, just mentioning in the product descriptions or documentation the minimum requirements for the product, and not limiting the code to run only when those requirements are met..

PHP comes with a useful to compare versions, since PHP 4:

version_compare( $version1, $version2, $operator )

And to get the current PHP version you can use

PHP_VERSION

predefined constant.

Using version_compare to run WordPress plugin on the correct requirements

It all depends the style and way you’re coding it. Each PHP component or method or function has a first appear version, so as you develop a plugin or a theme, you know the minimum require PHP version for a perfect setup.

An example is, using namespaces to correctly organize and load your plugin components, the minimum required version for namespaces is 5.3. Also, a good example is anonymous functions, which are introduced in same version also.

It is always handy and a good experience to alert the admin of a custom error on the screen using admin notices API (admin_notices action hook), while an install can not run your plugin correctly.

Here’s a custom notice preview:

admin notice preview

Here’s an example class to compare versions:

/**
  * Compare PHP versions
  * Making sure this blog is running enough required PHP software for
  * our plugin
  */

Class VersionCompare
{
    public $hasRequiredPHP;
    protected $min, $operator;

    public function __construct( $minVersion = '5.3', $operator = '>=' )
    {
        $this->min = $minVersion;
        $this->operator = $operator;

        if ( version_compare(PHP_VERSION, $this->min, $operator) ) {
            $this->hasRequiredPHP = true;
        } else {
            add_action( "admin_notices", array( $this, "notice" ) );
        }
    }

    public function notice()
    {
        printf(
            '<div class="error notice is-dismissible"><p>Plugin Name requires PHP %s %s.</p></div>',
            $this->operator,
            $this->min
        );
    }
}

A simple usage is instantiating the class in a custom variable, with first parameter set the PHP version we’re targeting (falls back to 5.3), and an optional operator which falls back to >= for comparing for greater than or equal our target min. PHP version.

$VersionCompare = new VersionCompare('5.4');

Now

$VersionCompare->hasRequiredPHP

will be set to true if the user has met the targeted version with the specified operator. If not, then, let’s make sure the theme or plugin stops right there and add an elegant error message on the admin screen:

if ( !isset($VersionCompare->hasRequiredPHP) || !$VersionCompare->hasRequiredPHP ) {
    return; // no min server requirements, stop, no more code below will be executed
}

Here, the full class code and a basic usage:

if ( !class_exists('VersionCompare') ) :

/**
  * Compare PHP versions
  * Making sure this blog is running enough required PHP software for
  * our plugin
  */

Class VersionCompare
{
    public $hasRequiredPHP;
    protected $min, $operator;

    public function __construct( $minVersion = '5.3', $operator = '>=' )
    {
        $this->min = $minVersion;
        $this->operator = $operator;

        if ( version_compare(PHP_VERSION, $this->min, $operator) ) {
            $this->hasRequiredPHP = true;
        } else {
            add_action( "admin_notices", array( $this, "notice" ) );
        }
    }

    public function notice()
    {
        printf(
            '<div class="error notice is-dismissible"><p>Plugin Name requires PHP %s %s.</p></div>',
            $this->operator,
            $this->min
        );
    }
}

$VersionCompare = new VersionCompare(); 

if ( !isset($VersionCompare->hasRequiredPHP) || !$VersionCompare->hasRequiredPHP ) {
    return; // no min server requirements, stop it right there
}

endif;

Now it is quite reasonable to add all the previous code to the top of your plugin/theme main loader file, for plugins, add it to the file where you specify the plugin details and properties e.g Plugin Name, Plugin URI..

Here’s an example:

<?php namespace BMC;
/*
Plugin Name: bbPress Messages Compose
Plugin URI: https://github.com/elhardoum/bbpm-compose
Description: bbPress Messages Compose
Author: Samuel Elh
Version: 0.1
Author URI: https://samelh.com
*/

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

if ( !class_exists('VersionCompare') ) :

/**
  * Compare PHP versions
  * Making sure this blog is running enough required PHP software for
  * our plugin
  */

Class VersionCompare
{
    public $hasRequiredPHP;
    protected $min, $operator;

    public function __construct( $minVersion = '5.3', $operator = '>=' )
    {
        $this->min = $minVersion;
        $this->operator = $operator;

        if ( version_compare(PHP_VERSION, $this->min, $operator) ) {
            $this->hasRequiredPHP = true;
        } else {
            add_action( "admin_notices", array( $this, "notice" ) );
        }
    }

    public function notice()
    {
        printf(
            '<div class="error notice is-dismissible"><p>Plugin Name requires PHP %s %s.</p></div>',
            $this->operator,
            $this->min
        );
    }
}

$VersionCompare = new VersionCompare('5.3'); // namespaces

if ( !isset($VersionCompare->hasRequiredPHP) || !$VersionCompare->hasRequiredPHP ) {
    return; // no min server requirements, stop it right there
}

endif;

use \BMC\Includes\Plugin, \BMC\Includes\Admin;

class BMC
{
    /** Constants **/
    public $constants;

    public function init()
    {
        // define constants
        $this->defineConstants();
    }

    # rest of code ...

There are many other implementations of this, checking for desired other plugins versions, or comparing WordPress version itself (

get_bloginfo('version')

), for making sure.

The point is, stop executing your product code when no minimal requirements are met, so as to avoid any bugs, conflicts, or errors on the screen bugging visitors and attracting attacks from robots. Instead, give a heads up to site admin to upgrade their software, or drop the plugin.

Allow/disallow user registration for email TLD

In this quick tutorial we will learn about allowing or disallowing WordPress user registration from custom TLDs and domain extensions for the user provided email.

You can either choose a set of TLDs and domain extensions to only limit and restrict registration to, or add a list of TLDs to prevent while processing user registration filtering the errors.

There is already Restrict User Registration free WordPress plugin which “Allows you to restrict registration for custom usernames, email addresses and custom email service providers”. This can be handy for denying registrations from specific email addresses or even email domains, so it is useful for this purpose.

Allowing registrations from custom TLDs:

In the following code, make sure to specify the extensions you want user registration to be restricted to, and place them into the array:

$allowed_tlds = array( "be", "nl" ); // allowing Belgium and Netherlands emails

Now as long as my email address is from a BE or NL email service provider site, then I am able to register.

Disallowing registrations from custom TLDs:

This process instead will deny registering users with specific TLDs while leaving the rest allowed. To use this, just add few extensions into the following array in follows-code.

$forbidden_tlds = array();

The code:

After you make your necessary edits, place this code into your child theme’s functions file, or with a custom plugin:

/**
* Plugin Name: Restrict mail TLD registration
* Plugin URI:  http://samelh.com
* Description: Allow/disallow user registration for email TLD
* Author:      Samuel Elh
* Author URI:  http://samelh.com
* Version:     0.1
*/

function se__verify_tld( $email ) {

	/* enter the TLDs to allow into following array, e.g array( "be", "nl", "cr" ) in lowercase */
	$allowed_tlds = array( "be", "nl" ); // allowing Belgium and Netherlands emails

	/* OR, enter few TLDs to prevent (forbidden) in lowercase */
	$forbidden_tlds = array();

	if ( empty( $email ) ) return false;
	$tld = strtolower( substr( $email, strpos( $email, '.' )+1 ) );
	if ( empty( $tld ) ) return true; // no tld caught, trigger error
	else $tld = strtolower( $tld );
	
	if ( !empty( $forbidden_tlds ) ) {
		if ( in_array($tld, $forbidden_tlds) ) {
			return true;
		}
	}

	else if ( !in_array($tld, $allowed_tlds) ) {
		return true;
	}

	return false;

}

add_filter( 'registration_errors', function( $errors, $user_login, $user_email ) {

	if ( se__verify_tld( $user_email ) ) {
		$errors->add( "tld_exception", "Sorry, you can not sign up with emails from this TLD" );
	}

    return $errors;

}, 10, 3 );

preview - Allowdisallow user registration for email TLD

Link to Github gist (downloadable 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