Author Archives: markn

About markn

Mark is the owner and founder of Timesheets MTS Software, an mISV that develops and markets employee timesheet and time clock software. He's also a mechanical engineer, father of four, and a lifelong lover of gadgets.

    Find more about me on:
  • googleplus
  • linkedin

Random Notes for Setting up a New Ubuntu LAMP Server on Linode

This is the first entry written on the new home of this blog, a 1GB Linode server. I moved it here from a Pair account that I am in the process of closing. The server is running Ubuntu Server 16.04 LTS, Apache, MySQL, and PHP 7.

Using /srv/www as Web Root

It looks like Apache defaults to using /var/www as the web root. I prefer using /srv/www. To enable this open up /etc/apache2/apache.conf, comment out the section and uncomment the section. Make sure to change the AllowOverride directive to All or .htaccess and mod_rewrite won’t work.

Getting mod_rewrite to Work

You need to make sure that the AllowOverride directive is setup as per above. Then just enable the mod with a2enmod rewrite.

Installing PHP7

PHP7 is installed fairly easily with sudo apt-get install php7.0 php7.0-fpm php7.0-mysql.

Permissions for WordPress

When first installing WordPress make sure to

sudo chown -R www-data:www-data /srv/site-root/

After installation set permissions on folders and files using:

find . -type d -exec chmod 775 {} \;
find . -type f -exec chmod 644 {} \;

Note that 775 is used because we need www-data and a ftp user to be able to modify the WordPress install.

Then take ownership of the whole folder again with:

sudo chown -R ftp-user:ftp-user /srv/site-root/

And give ownership of wp-content to Apache with:

chown -R www-data:www-data /srv/site-root/wp-content

And secure wp-config.php with:

chmod 600 /srv/site-root/wp-config.php

Setting up a MySQL Database and User

It like to have a user for each WordPress install rather than using some sort of global user. It’s done fairly easily with:

create database database_name;
create user user_name@’localhost’ identified by ‘somepassword’;
grant all privileges on database_name.* to user_name@’%’ identified by ‘somepassword’;
grant all privileges on database_name.* to user_name@localhost identified by ‘somepassword’;
flush privileges;

Zipping a WordPress Install

Moving a WordPress install from Pair to Linode involved zipping up the install folder, transferring it via SFTP to the new server, and unzipping it. This is done with:

tar -zcvf somesitename.tar.gz somesitename/

And extracting with:

tar -xvzf somesitename.tar.gz

The extraction will create the folder to put the contents in.

Basic Process of Moving Site

  1. Zip up WordPress install folder.
  2. Dump database using MySQL Dump or phpMySQL. Remember to add use database_name; to start of MySQL Dump file.
  3. Transfer install folder and MySQL dump to new server using SFTP.
  4. Create new database and user. Import data to database using something like mysql -uroot -ppassword < mysql_dump.sql
  5. Unzip WordPress tar.gz file to /srv.
  6. Edit the /srv/domain_name/wp-config.php file to enter new database details and login.
  7. Create new /srv/domain_name-logs/ folder for Apache logs and set www-data as the owner.
  8. Set permissions on WordPress folders as covered earlier.
  9. Make entry into /etc/hosts file for site_name.
  10. Create /etc/apache2/sites-available/ file making sure to set the web root and log file path correctly.
  11. Enable the site with a2ensite
  12. Test by setting your local machine’s hosts file for the domain to the server IP address.
  13. Setup the new DNS Zone on Linode for the site.
  14. Adjust the Name Servers at the domain registrar for the domain.
  15. Wait for the DNS records to propagate!

How Not to Mess Up a Multi Domain SSL Renewal

I’ve just finished a few hours of fun messing up a multi domain SSL certificate renewal. In the first draft of this entry I did not use the word mess, messing, or messery. I used another four letter word, but since then I’ve settled a bit and have come back and edited out the profanity. I managed to un-mess it today so in the interests of not performing the same messery at some point in the future here’s what I need to do next time.

  1. The Namecheap Multi-Domain PositiveSSL certificate requires you to have a certificate for the non-www and the www versions of a domain. In fact, it requires you to have one for each sub-domain of a parent domain you want to secure.
  2. If renewing the SSL certificate then you should generate a new CSR file. Make extra sure to use the non www version of the domain as the primary domain.
  3. Using the Namecheap HTTP DCV validation method is dead simple but make sure to read the instructions carefully as they can (and have) changed the folder they want the validation file uploaded to. It was just the root folder but just a few weeks later they wanted it in the ./well-known/pki-validation/ folder. What the?!

When a new certificate is issued make sure to upload the crt and ca-bundle file to the /etc/apache2/some-folder-name-date/ folder to keep it apart from your old certificate files. Don’t forget to put the server .key and .csr file there and then protect the lot with chmod 400

Using the Apache VirtualHost system requires the following sort of entry for a site:

<VirtualHost *:80>
    ServerAlias *
    DocumentRoot /srv/www/public_html/
    Redirect permanent /

<VirtualHost  *:443>
    SSLEngine On
    SSLCertificateFile /etc/apache2/ssl/domain-com-august-2017/some_file.crt
    SSLCertificateKeyFile /etc/apache2/ssl/domain-com-august-2017/server.key
    SSLCertificateChainFile /etc/apache2/ssl/domain-com-august-2017/
    ServerAlias *
    DocumentRoot /srv/www/public_html/
    ErrorLog /srv/www/domain-logs/error.log
    CustomLog /srv/www/domain-logs/access.log combined
    AddHandler cgi-script .cgi .pl

Now, I am not 100% sure if the ServerName/ServerAdmin/ServerAlias/DocumentRoot entries need to be duplicated. But until this point it’s never broken anything so no harm done. The key point of this is that because the SSL handshake is the very first thing that happens between a client the web-server you absolutely 100% need a SSL certicate for the www subdomain as well as the main domain. There’s no getting around it with redirects in .htaccess or any other such trickery.

Allowing ‘0000-00-00’ as Default Value for MySQL datetime Column

It can be useful to allow the default value of a MySQL date / datetime column to be set to zero, it makes checking if it’s initialised simple and saves you having to deal with pesky null values. It also allows you to set the column as NOT NULL. However, from version 5.6 onwards MySQL has STRICT_MODE for queries turned on, as well NO_ZERO_IN_DATE mode turned on. If you try to create new column with something like:

`some_field` datetime DEFAULT ‘0000-00-00 00:00:00’

You’ll see this error:

Invalid default value for ‘some_field’

This is pesky if you’ve got code written for earlier versions of MySQL and you don’t want to go back and re-write it all to check for NULL instead of zero. Turning these things off is pretty simple. On Ubuntu I created a file in /etc/mysql/conf.d/ called disable_strict_mode.cnf. The file contents are shown below:


Then restart MySQL with sudo service mysql restart and you’ll be able to create date and datetime fields with a zero default value.

WooCommerce – Custom Product Fields and WooCommerce Fields Factory

WooCommerce does have ‘attributes’ available for products but like most things in WC these are tailored for users selling clothing and various other consumer products. In my application each product is unique and once sold gone forever. Each product has common characteristics though, such as year of manufacture, metal it is made from, and where it was manufactured. As a matter of course the user of the WooCommerce store wanted to define a set of characteristics that would be entered when a new product was created and then wanted those characteristics displayed in the product listing. A good solution was arrived at through the use of the WooCommerce Fields Factory plugin and modifying the Storefront Single Product->Tabs->description.php template.

The WC Fields Factory plugin allows you to add custom fields to your WooCommerce products. It has the added benefit of allowing you to display those fields on the admin side where they can be edited at the product level by your shop admins. Nifty. In my application I created a group of Admin Fields called “Coin Attributes” and then populated the group with the fields that the customer wanted for their products.

Once that was done and I made sure to check the Hide in Product Page and Hide in Cart & Checkout Page for each field. Now when I add a product I see this in the General tab for each product:

So now we can store the data with each product as needed. The next step is to make sure the data displays as required for products. The key is that we only want the data field to display if there’s a value set. To do that we just need to do a bit of tweaking to the Storefront Single Product->Tabs->description.php template. We do that by making a copy of it into the /woocommerce/single-product/tabs folder of our child theme. In our case we just wanted to display the data in a table. So the PHP code required looked like this:

    //get the custom field data using the get_post_meta function.  All WC Field Factory fields are prefixed with wccaf_ and then the field name
    $coin_type=get_post_meta( $post->ID, "wccaf_coin_type", true );
    $krause_number=get_post_meta( $post->ID, "wccaf_krause_number", true );
    $mint=get_post_meta( $post->ID, "wccaf_mint", true );
    $mintage=get_post_meta( $post->ID, "wccaf_mintage", true );    

//display information only if there's a value set
    if (strlen($coin_type)>0)
        $coin_information.='<tr><td class="coin-information"><strong>Coin Type:</strong></td><td class="coin-information">'.$coin_type.'</td></tr>';
    if (strlen($krause_number)>0)
        $coin_information.='<tr><td class="coin-information"><strong>Krause Number:</strong></td><td class="coin-information">'.$krause_number.'</td></tr>';    
    if (strlen($mint)>0)
        $coin_information.='<tr><td class="coin-information"><strong>Mint:</strong></td><td class="coin-information">'.$mint.'</td></tr>';         
    if (strlen($mintage)>0)
        $coin_information.='<tr><td class="coin-information"><strong>Mintage:</strong></td><td class="coin-information">'.$mintage.'</td></tr>';  

    if (strlen($coin_information)>0)
        echo '<table class="coin-information">'.$coin_information.'</table>';

Of course where exactly you echo out the data in the TABS template is up to you and determined by how your product pages are laid out.

WooCommerce – Display SOLD Instead of Price for Sold Products

If you’re using WooCommerce and selling one-off products you don’t necessarily want to use the default terminology for sold out products. You might want to hide the sold price and remove the WooCommerce “In Stock” messages that are better suited to sites selling t-shirts and so on. Removing the stock availability messages is simple enough using the woocommerce_get_availability filter.

    add_filter('woocommerce_get_availability', 'my_get_availability', 1, 2);

    function my_get_availability($availability, $_product)
        $availability = '';

Couldn’t be any simpler, just set the availability text to an empty string.  Presto chango those messages are gone.

Replacing the price with SOLD when a product is sold is almost as simple, it just requires hooking into a couple more filters.

    add_filter("woocommerce_variable_sale_price_html", "my_remove_prices", 10, 2);
    add_filter("woocommerce_variable_price_html", "my_remove_prices", 10, 2);
    add_filter("woocommerce_get_price_html", "my_remove_prices", 10, 2);
    function my_remove_prices($price, $product)
        if (!$product->is_in_stock())
            $price = "SOLD";
        return $price;

WooCommerce – Add Phone Numbers to Header of Storefront Theme

I happen to like web stores that have contact details in prominent locations so it’s useful to be able to add phone numbers to the header of a site using WooCommerce. In my particular application I’m using the Storefront theme.  Firstly you need a  bit of CSS in your child theme’s style.css file. Your widths might be different and I’ve adjusted the margin of the site-search div to compress things a bit.

.header-contact-details {
    width: 50.7391304348%;
    float: right;
    margin-right: 0;
    clear: none;


Then you can use the storefront_header action to insert some code into the header.  To set the priority of the insertion have a look at the storefront header.php file.  In  my case I wanted the new HTML in between storefront_product_search and storefront_primary_navigation_wrapper so I set the priority to land in between those two.

//add phone numbers to header

    add_action('storefront_header', 'storefront_add_header_phone', 41);

    function storefront_add_header_phone()

        <div class="header-contact-details" style="text-align:right"><p style="color: rgb(64, 48, 74);font-weight:600;font-size:20px;"><i class="fa fa-phone" style="color:rgb(155, 126, 230)"></i> 555 555 555 (AUS)  <i class="fa fa-phone" style="color:rgb(155, 126, 230)"></i> +61 555 555 555 (INTL)</p></div>

The code is pretty clear, I make use of the font awesome font set to have some pretty phone icons and colour them to match the rest of the site style. Of course there’s a little bit of CSS inline here that could be moved into the style.css file but for now it works and I am happy with that. Here’s the what the end result looks like:

WooCommerce – Receive Copies of Customer Emails

If a user is new to WooCommerce it can be a little confusing what emails a customer is receiving and exactly what events are triggering the delivery of emails. To help alleviate the problem it can be useful to deliver copies of all customer emails (see the WooCommerce Email Notifications screen below) to another email address so they can be monitored.

WooCommerce Email Notifications Screen

In this case the woocommerce_email_headers() filter is useful. It allows you to intercept the email header and inject your own custom headers of the same form used by the PHP mail function. In my case I was happy to include a CC email address so I used this code:

//Send copies of customer emails to
add_filter( 'woocommerce_email_headers', 'copy_customer_emails_headers_filter_function', 10, 2);

function copy_customer_emails_headers_filter_function( $headers, $object ) 
    if ($object == 'customer_completed_order' || $object == 'customer_invoice' || $object == 'customer_note' || $object == 'customer_on_hold_order' || $object == 'customer_processing_order' || $object == 'customer_refunded_order') 
        $headers .= "CC: Someone <>" . "\r\n";

    return $headers;

The code itself is fairly self explanatory. It checks the passed object and if it’s one of the customer notification emails then the “CC: Someone <>” . “\r\n” string is appended to the email headers. That email address will now receive a copy of all the emails sent to a customer. Of course the customer will see the CC address in their emails so you could include a BCC address with something like “Bcc: Someone<>\r\n”.

WooCommerce – Default Manage Stock and Sold Invidually Values

I’ve had the somewhat dubious pleasure of launching a new WordPress / WooCommerce site for an established collectables business. The site replaced an aging cart system, that was frankly a steaming pile. Almost every item sold by this business is unique so they wanted to default the values for Manage Stock to true and Sold Individually to true.

The WooCommerce Inventory Screen

It turns out the easiest way to do this is to add some javascript to the admin_enqueue_scripts action that checks the form elements we need checked. Poking about with the Chrome element inspector at the WooCommerce stock handling form told me we needed the _manage_stock and _sold_individually checkboxes ticked and to set a value in the _stock text field. Here’s the code I put in my child theme’s functions.php file.

//set default stock handling values

    add_action('admin_enqueue_scripts', 'wc_default_variation_stock_quantity');

    function wc_default_variation_stock_quantity()
        global $pagenow, $woocommerce;

        $default_stock_quantity = 1;
        $screen = get_current_screen();

        if ( $pagenow == 'post-new.php' && $screen->post_type == 'product')
            <script src="//"></script>
            <script type="text/javascript">
                jQuery(document).ready(function () {
                    if (!jQuery('#_manage_stock').attr('checked')) {
                        jQuery('#_manage_stock').attr('checked', 'checked');
                    if ('0' === jQuery('#_stock').val()) {
                        jQuery('#_stock').val(<?php echo $default_stock_quantity; ?>);
                    if (!jQuery('#_sold_individually').attr('checked')) {
                        jQuery('#_sold_individually').attr('checked', 'checked');


The code itself is fairly simple. The javascript is injected into the document.ready function but only if we’re adding a new post and the post type is WooCommerce’s product post type. The appropriate checkboxes have their checked attribute set to true and the _stock field is set to a value of 1 if it’s currently zero. Simples!

Munich Dunkel

With summer coming to an end I am looking for a darker winter beer to drink that is an easy quaff. Sticking with my recent history of German styles I bought a couple of commercial Dunkels and liked what I tasted. So Dunkel it is. The Dunkel is a dark, transparent lager brewed with darker Munich malts and sometimes some pilsner malt. It is supposed to have a sweeter dark malt profile with a moderate spicy hop aroma. I surfed around various beer recipe sites before settling on a simple recipe that I thought would suit my palate. I adapted the recipe using the trusty BIAB recipe spreadsheet tweaking it for my usual no chill / 30 minute technique.

13 February 2017

Munich Dunkel

12.5L BIAB

Munich 1 Malt 1.4kg (11.29EBC)
Pilsner Malt 1.4kg (4.56EBC)
CaraAmber 0.25kg (9.96EBC)
Carafa 2 0.1kg (35.15EBC)

Pitch onto Fermentis SAFLAGER 34/70 yeast cake from a previous Bohemian Pilsner brew.

15L strike water at 72C with 69C target mash temp. Stir every 20 minutes of mash. The temperature at the start of the mash was 67.6C and at the end of the mash the temperature was 65.2C. At that point I squeezed the bag and sparged it with 4L of cold water. This resulted in 17.5L of pre-boil wort with an SG of 1.036 at 50C. Temperature corrected to 1.045.

30 minute boil

6g Nugget 15.5%AA @ 30 minutes
20g Saaz 4.4%AA @ 10 minutes
1/4 of a Whirlfloc tablet @ 10 minutes.

This hop schedule gave a projected bitterness right at the bottom limit of the style. Because I no chill I’ve perceived that a lot of my beers have been too bitter even when counting flameout and 5 minute additions as 10 minute additions. So when devising this recipe I used those allowances AND aimed for IBU’s at the bottom limit of the style hoping it would end up right on target.

As usual when the boil was done I stuck the whole pot in a my fermentation fridge and allowed it to get the wort down to 12.5C. The next day when it was at the correct temperature I tipped the lot onto the yeast cake of my last brew and aerated it vigorously with my giant brew spoon. Original gravity was finalised at 1.050. There was 14.5L of wort in the fermenter. I’ll be using the usual fast lagering method for this beer before bulk priming and bottling.

Fermentation Log

14-2-17 – Into fridge at 12C
27-2-17 – ramp temp up to 20C over three days
2-3-17 – Cold crash to 3C
4-3-17 – Bottle with 130g of priming sugar to get 18.5 740mL bottles. FG measured at 1.019 which was too high. Perhaps due to not leaving at 20C long enough?


Well I couldn’t wait with this one and opened a bottle just 8 days after bottling and surprisingly it was reasonably carbonated and poured fairly clear. It had a lovely creamy tan head which didn’t hang around too long most likely due to it not being in the bottle long enough. It was super easy to drink and very toasty with just a hint of the dark malt burnt flavor which is not really appropriate for the style. To my palate the bitterness was spot on and it went down just a bit too quickly! I don’t see this brew hanging around too long and can’t wait to try it again to iron out a couple of wrinkles in the process and perhaps tone down the impact of the Carafa 2 specialty malt a little bit.

Munich Dunkel

Bohemian Pilsner with Flameout Hop Addition

This is the third time I’ve brewed this particular Bohemian Pilsner recipe with the only variation being a flameout hop addition of Czech Saaz. Last time I brewed it the hop flavour was a bit grassy and overpowering, so I’ve dropped the addition to 25g down from 35g last time.

29 January 2017

Bohemian Pilsner

12.5L BIAB

3kg Gladfield Pilsner Malt (3.8EBC)
0.26kg Weyermann Vienna Malt (7.5EBC)
Fermentis SAFLAGER 34/70

15L strike water at 72C with 69C target mash temp. Stir every 20 minutes of mash. At the end of the mash the temperature was 67C. At that point I squeezed the bag and rinsed it with 4L of cold water. This resulted in 17.5L of pre-boil wort with an SG of 1.037 at 51C. Temperature corrected to 1.046.

30 minute boil

7g of Warrior 15.5%AA @ 30 minutes
17g Saaz (4.4%AA) @ 15 minutes
15g Saaz (4.4%AA) @ 10 minutes
1/2 whirlfloc @ 10 minutes
25g Saaz (4.4%AA) @ Flameout.

I followed my usual no chill routine to get the wort down to 12.5C. At that point I poured it into my sterilised fermenter and took at gravity reading of 1.053. There was 13.75L of wort in the fermenter which I aerated and then sprinkled the yeast on top. This will be using the Brulosopy Fast Lager method which means leaving it at 12.5C for 5 days before allowing the temperature to ramp up to 20C over 2 days, holding at 20C for a week or so before fining with gelatin and cold crashing.

Fermentation Log

30/1/17 – Set temp of 12.5C.
6/2/17 – +3C each day until 20C
8/2/17->11/2/17 – 20C
11/2/17 – Fine with gelatin
11/2/17->13/2/17 – Cold crash to 1C

13/2/17 bottled after bulk priming w/130g of sugar. Forgot to take final gravity reading before bulk priming, gravity after bulk priming was 1.019.