Make a Pi-Hole Work with a Bell Giga Hub

Due to a recent 10-block move there were shenanigans and my previous ISP was incapable of transferring my service to our new address. As a result, I signed up for the much faster Bell Fibe, fibre-to-the-home service. Now I have a 1.5 Gigabit internet connection, and pay less than I did for the 25 megabit connection. Sweet! I had some trouble getting my Pi-Hole up & running so I’m sharing what I did in case others have similar problems.

The Previous Setup

My old setup consisted of a DSL modem in bridge mode and an TP-Link Archer C7 router V2 that handled all networking tasks except for responding to DNS queries. The C7’s DHCP settings pointed to the Pi-Hole as the main DNS server for the network, and it worked great.

The Plan: Giga Hub + Pi-Hole

Included with my new service is a Bell Giga Hub, which is a combination ONT and router, (with a 10 gigabit ethernet port and wifi 6e!). Despite some complaints in /r/bell it seems like a very capable device, so I planned to use it as the main network device, and try to use it to point all devices on the network to a Raspberry Pi running Pi-Hole.

The Roadblock

Despite a place in the Giga Hub’s admin interface where it looks like I should be able to point the DNS at the Pi-Hole, I couldn’t get it to work. Every time I pointed DNS at the Pi-Hole the Pi-Hole couldn’t access the internet. It couldn’t even ping an IP address.

A screenshot of the Bell Giga Hub's dns settings
It looks like I should be able to set a DNS server here… but it doesn’t work.

Side Quest: Restoring Internet Access to the Pi-Hole

I ended up with a Raspberry Pi that couldn’t access the internet, which wasn’t ideal. The Pi was accessible on the local network, so I initially restored its internet access by changing it’s IP address. I later realized that turning setting the Giga Hub’s DNS back to “Obtain DNS information automatically” and changing or deleting the IP address in the “Manually specify DNS information” would restore the Pi’s internet access. It seemed that even though the router was using external, upstream, DNS, it was still doing something weird with the IP address in the inactive “Manually specify DNS information” screen.

DHCP to the Rescue

The solution ended up being relatively simply. Pi-Hole has the option to use the Pi-Hole as a DHCP server, and it is smart enough to tell connecting devices to also use the Pi-Hole for DNS. So I turned on the Pi-Hole’s DHCP server and configured it to allocate IP addresses in the same range as the Giga Hub, then turned off the DHCP server on the Giga Hub and everything worked. DHCP is a broadcast service so there is no configuration telling clients where to find the server. If there’s a DHCP server on the network the devices will find it.

Some posts in /r/bell had me worried that I would have to either use my C7 or another, faster, router in PPPoE mode, but switching to the Pi-Hole as the DHCP server was enough. That’s great because I didn’t want to buy another fast router, or use my older C7 when there’s a perfectly capable Wifi 6e router in the Giga Hub.

When I set this all up my Giga Hub was on Firmware version 1.14.something. The firmware was recently updated to version 1.16 and is still working. It may be that Firmware 1.16 also fixes the problem I had setting the DNS server on the Giga Hub, but what I have is working, and if it ain’t broke don’t fix it.

A screenshot of my pi hole admin panel showing that it is processing thousands of DNS queries.
My Pi-Hole is processing thousands of DNS queries, (yes, I know blocking is off at the moment).

Laravel Envoyer Notifications in Zoho Cliq

I recently switched a project from a home-grown deployment script to Laravel Envoyer. While the homegrown script could maybe have been adapted it would have taken time, and had to be maintained, and Envoyer offers some useful extras like Slack, Discord, and Microsoft Teams integration, and heartbeat monitoring.

Except we don’t use Slack, Discord, or Teams, we use Zoho‘s Slack competitor, Cliq.

Comparing Slack’s web hooks with Cliq we see that a bot is needed in Cliq, but making one is super-easy, on the order of four or five clicks.

Once there is a bot it can receive web hooks. Figuring out the right way to provide an authentication token was weird, there is not clear documentation, but the reply from Eric Hirst in in the Zoho forum thread announcing Cliq bots has a solution that works.

Now we can receive web hooks, I tried putting the bot’s web hook URL in for both Slack notifications and Discord notifications in Envoyer, and I got notifications. Both systems expect a POSTed JSON object, so that’s what they get. The Discord one is simpler one of the properties is simply markdown text, which Cliq understands, so the Cliq incoming web hook handler needs to grab that markdown text and return it so the bot will post the message in Cliq.

// Configure the bot's incoming webhook URL in any third-party service to trigger this handler.
response = Map();
message = body.toMap();
if(message.containKey('content'))
{
	response.put("text",message.get('content'));

	// If you want the bot to post to one of your channels, and want it to appear
	// as the bot posting, add the bot info like this. Otherwise it the message 
	// will be "from" the person who owns the auth token, with a small "bot" flag next to it.
	response.put('bot',{"name":"Envoyer","image":"https://envoyer.io/img/favicons/apple-touch-icon-120x120.png"});

	// If you want to post to a channel this is how. If you don't do this the bot
	// will simply post the message to any chats it has open.
	zoho.cliq.postToChannel('general',response);
}
return response;

The beauty of simply dumping the markdown into the chat is that this simple code handles all notifications from Envoyer, including successful & failed deployments and heartbeat notifications. You can look for specific text and alter your notification if you want, (I have a big headline for successful deployments), but it’s not needed.

It would be easier to test this if there was a way to trigger a test notification in Envoyer, but Envoyer isn’t really something to tinker with, it’s supposed to just work. It would also be great if Envoyer could support Zoho Cliq directly, but I’m not sure many people in the Laravel community are using Zoho.

So now we have Envoyer notifications in Zoho Cliq in our organization. If you’re one of the few Laravel / Zoho unicorns out there like me you can have them too.

Remove a model’s Global Scopes in Laravel Nova

I am working on a Laravel project that has moderated reviews. Most of the time we only deal with reviews that have been approved, so the Review model has an ApprovedScope Global Scope to only show approved review, but staff members need to see un-approved reviews in our Nova admin so that we can approve, (or reject), reviews. Removing a Global Scope from a model for all of Nova is trickier than it appears at first glance, but there’s at least one way to do it, and maybe more.

Things that do not work

Call withoutGlobalScopes() in the Nova::serving() callback

Googling “remove global scope nova” turns up a post on Medium called Add or remove global scopes in Nova. Sounds promising! It claims we can remove global scopes by adding this to the boot method of the `NovaServiceProvider:

class NovaServiceProvider extends NovaApplicationServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Nova::serving(function () {
            \App\Rating::withoutGlobalScopes();
        });
    }
}

Cool! But it doesn’t work. When adding a global scope to a model using the model’s boot method like the Laravel documentation shows, the model’s boot method is called after the Nova::serving() callback. So in this situation we remove nothing from the Rating, then add a global scope later when the model is booted.

Call withoutGlobalScopes() in the model’s booted event

If a model has a global scope added during its boot then we should be able to remove it during the ‘booted’ event – or at least that’s what I thought, so I tried this in NovaServiceProvider::boot :

Nova::serving(function() {
    Event::listen('eloquent.booted: App\Rating', function($rating) {
        Rating::withoutGlobalScopes();
    });
}); 

No dice. I’m not certain why this doesn’t work, but some XDebug spelunking shows me that:

  1. There’s no withoutGlobalScopes() method on Laravel models, but when the model gets a method call that it doesn’t recognize it passes it to the model’s Builder instance.
  2. The Builder does have a withoutGlobalScopes method, so we don’t get an InvalidMethodException. In the Builder’s withoutGlobalScopes call the ApprovedScope is removed from the Builder’s list of global scopes.
  3. But the Global Scope is still applied to the query. My best guess is that a different query builder is used to actually generate results, or that later, when the models are about to be retrieved from the DB, the model re-passes the list of Global Scopes to the builder – and since we haven’t removed the Global Scope from the model it gets re-added to the Query Builder. If anyone knows what’s actually going on here I would love to know in the comments or on Twitter.

Something that does work

We need to remove the Global Scope after the model has booted and prevent it from being passed or re-passed to the Query Builder. How about a Model::withoutGlobalScopes method? The model’s addGlobalScope method comes from the Illuminate\Database\Eloquent\Concerns\HasGlobalScopes trait, and stores the global scopes in a static::$globalScopes[static::class] array. Creating our own HasRemovableGlobalScopes trait, with withoutGlobalScope and withoutGlobalScopes methods that mirror the signature of the Illuminate\Database\Eloquent\Builder withoutGlobalScope and withoutGlobalScopes methods can solve the problem, (also available as a Gist):

<?php

namespace App\Concerns;

use Closure;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Arr;

trait HasRemovableGlobalScopes {

	/**
	 * @param  \Illuminate\Database\Eloquent\Scope|string  $scope
	 */
	public static function withoutGlobalScope( $scope )
	{
		if (is_string($scope) && is_array(static::$globalScopes[static::class])) {
			Arr::forget(static::$globalScopes[static::class], $scope);
		} elseif ($scope instanceof Closure) {
			Arr::forget(static::$globalScopes[static::class], spl_object_hash($scope));
		} elseif ($scope instanceof Scope) {
			Arr::forget(static::$globalScopes[static::class], get_class($scope));
		}
	}

	/**
	 * @param \Illuminate\Database\Eloquent\Scope[]|string[] $scopes
	 */
	public static function withoutGlobalScopes( array $scopes = [])
	{
		if(empty($scopes)) {
			static::$globalScopes = [];
		} else {
			foreach($scopes as $scope) {
				static::withoutGlobalScope($scope);
			}
		}
	}
}

The withoutGlobalScope method mirrors the HasGlobalScopes::addGlobalScope method to remove a single global scope, and the withoutGlobalScopes method can accept an array of global scopes to remove or be called with no parameters to remove all global scopes, (the same as Builder::withoutGlobalScopes).

A drawback?

The fact that we have to do this to remove global scopes for Nova seems to be an oversight. I’m hopeful that withoutGlobalScope/withoutGlobalScopes methods will be added to future versions of Laravel. If that happens there will be a method name collision between the HasRemovableGlobalScopes trait’s methods and the first-party ones, so read the release notes if you’re going to use this method.

Something else that might work

After writing the HasRemovableGlobalScopes trait I realized it should be possible to create an additional global scope that undoes my original global scope. In this case something like this might work:

Nova::serving(function() {
    Event::listen('eloquent.booted: App\Rating', function($rating) {
        Rating::addGlobalScope(new AllRatingsScope);
    });
}); 

Where AllRatingsScope says to include all ratings. I haven’t tried this so don’t know what happens when there are two global scopes that specify opposite conditions. You might get no results, or the scope that’s applied last might win. Assuming that the scope applied last wins it’s still important to use the eloquent.booted event to make sure the “undo” scope is added after the original scope.

All together now

I’m sticking with the HasRemovableGlobalScopes trait. I feel like actually removing the Global Scope is more logical than adding another scope to undo what the first one does. Using the HasRemovableGlobalScopes trait, this is what my whole system looks like:

Apply the original global scope to the Rating, (or wherever you need a global scope), and use the HasRemovableGlobalScopes trait:

class Rating extends Model
{
	use HasRemovableGlobalScopes;

	// ... 

	protected static function boot()
	{
		parent::boot();
		static::addGlobalScope(new ApprovedScope);
	}

	// ...
}

Set the event listener in the NovaServiceProvider’s boot method to remove the global scope for Nova:

class NovaServiceProvider extends NovaApplicationServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
	    Nova::serving(function() {

		    Event::listen('eloquent.booted: App\Rating', function($rating) {
			    Log::info('rating booted ' . request()->url());
		    	Rating::withoutGlobalScopes();
	        });
	    });

	    parent::boot();
    }
}

It works well for me. A possible enhancement would be to have my own App\Model class that extends Laravel’s Model and uses HasRemovableGlobalScopes, then extend my other models from that. At the moment I don’t have my own App\Model, and don’t have a lot of places where I need to share between all my models, but if I find I’m sprinkling too many use HasRemovableGlobalScopes lines around my code base I’ll make the change.

Changing the Name and Path of the Active WordPress Theme Without Breaking Theme Settings

After changing a brand name, but little else, on a WordPress site we can end up with a WordPress theme called “oldbrand” on a site called “New Brand.” This leads to weird things like the brand’s logo being at the URL newbrand.com/wp-content/themes/oldbrand/img/newbrand-logo.png. Oops. SEOs must cry a little when they see something like that. This is a situation one of my clients ended up in after a name change, but it ends now.

We can’t simply change the theme directory from themes/oldbrand to themes/newbrand and update the active theme in the Appearance section of the wp-admin because theme-related settings are tied to the theme directory in the database. This includes things like widget placement; a different theme probably has very different widget areas so widget placement should be stored per-theme. The same goes for any theme settings & customizations. To get around this we need to update the database so WordPress thinks that the new theme name is the theme name we’ve always been using.

A word of caution: back up your database before trying this. We’re going to run raw queries. If something breaks you may end up in an in-between limbo, and restoring from backup is the quickest way out.

After diving through the database of a few sites this morning these queries make it possible to rename the theme directory, and optionally rename the theme in style.css:

# Update the main theme options
UPDATE wp_options SET option_value='new-theme-directory' WHERE option_name='template';
UPDATE wp_options SET option_value='new-theme-directory' WHERE option_name='stylesheet';
UPDATE wp_options SET option_name='theme_mods_new-theme-directory' WHERE option_name='theme_mods_old-theme-directory';

# If also updating the Theme Name in the theme's style.css
UPDATE wp_options SET option_value='New Theme Name' WHERE option_name='current_theme';  

# If any posts reference assets in the theme such as images or logos.
UPDATE wp_posts SET post_content=REPLACE(post_content, 'themes/old-theme-directory', 'themes/new-theme-directory' );
UPDATE wp_postmeta SET meta_value=REPLACE(meta_value, 'themes/old-theme-directory', 'themes/new-theme-directory');

# If WordFence is used
UPDATE wp_wfConfig SET val=REPLACE(val, 'themes/old-theme-directory', 'themes/new-theme-directory');

# Delete a couple of transients that store references to the old theme directory.
DELETE FROM wp_options WHERE option_value='_site_transient_theme_roots';
DELETE FROM wp_options WHERE option_value='_site_transient_update_themes';

Protips:

  • Try to run these queries at nearly the same time as the theme directory is renamed. Bonus points for making a script that does it all nearly instantly.
  • If an installation stores transients somewhere other than the DB, then clearing them in the DB won’t work, (clearing the transients may not be strictly required).
  • The sample code uses the default wp_ table prefix. When working on a site with a different prefix, use that.

Hopefully this helps someone finish their Googling session and get on with a rename!

Preview Laravel Password Reset and Verification E-mails in a Browser

I’m building something on Laravel, using the built-in Auth with 5.7’s new E-mail Verification feature. Today’s task was to tweak the design of E-mail notifications a bit, so I wanted to preview them in a browser. I thought it would be easy, but the built-in E-mails use the Notification system, bypassing Mailable objects. Some Googling didn’t help much, but combining the Notifications documentation and this forum post got let me preview the messages. Hopefully this post can help someone else preview their messages much more quickly than I did today.

The concept is similar to the “Previewing Mailables” documentation: define a route to view your preview on, and return something that will render the message. The trick is that the MailMessage objects that the Notifications’ toMail() methods return don’t have a render() method like Mailable class does, so we need to find something else. Since the built-in Auth uses Markdown mail we can use the Markdown class. A Notifiable object, (the User object for the user who will get the notification), and a Notification are also needed.

For this to work you need to have the right Notifications published to your resources/views/vendor directory. This is done with the Artisan command php artisan vendor:publish --tag=laravel-notifications.

And set up the preview routes:


Route::get('/reset-notification', function () {

    // Get a user for demo purposes
    $user = App\User::find(1);

    // Make a Reset Notification object, (subclass of `Notification`)
    $resetNotification = new \Illuminate\Auth\Notifications\ResetPassword( 'some-random-string-this-will-be-much-longer-in-real-life' );

    // get the `MailMessage` object
    $mailMessage = $resetNotification->toMail($user);

    // get the `Markdown` object
    $markdown = new Illuminate\Mail\Markdown(view(), config('mail.markdown'));

    // Render the vendor.notifications.email view with data from the `MailMessage` object
    return $markdown->render('vendor.notifications.email', $mailMessage->toArray());
});


Route::get( '/verify-notification', function () {

    // Get a user for demo purposes
    $user = App\User::find(1);

    // Make a Verify Notification object, (subclass of `Notification`)
    $verifyNotification = new Illuminate\Auth\Notifications\VerifyEmail();

    // get the `MailMessage` object
    $mailMessage = $verifyNotification->toMail($user);

    // get the `Markdown` object
    $markdown = new Illuminate\Mail\Markdown(view(), config('mail.markdown'));

    // Render the vendor.notifications.email view with data from the `MailMessage` object
    return $markdown->render('vendor.notifications.email', $mailMessage->toArray());
});

Once the Notification is published and the routes created it is possible to preview the Password Reset and E-mail Verification E-mails in the browser.