I enjoy InvoiceShelf since I migrated to it in December 2024. InvoiceShelf is open-source invoicing software. I do not allow it to send emails, but I’m exploring the idea.
How to prevent local web apps from sending emails
I run MailHog. It’s an open-source email catcher that allows me to see what emails sites running on my computer have tried to send.
Configure InvoiceShelf to use MailHog
Go to Settings > Mail Configuration.
Mail Driver: smtp
Mail Host: 127.0.0.1
Mail Port: 1025
Mail Encryption: none
Screenshot
Here’s a screenshot of my Mail Configuration page.
Create a directory for the site & install Drupal files
All my sites live in ~/sites, so I navigated there in macOS Terminal and ran these commands. I want my Drupal site in a folder named p8hd and my local website to live at https://p8hd.test.
composer create-project drupal/recommended-project:10.4.1 p8hdcd p8hd
valet link
valet secure
This creates a directory at “~/sites/p8hd”, populates it with composer.json and composer.lock files, installs the composer dependencies, creates a symbolic link, and creates a TLS certificate. A website is now live at the URL https://p8hd.test.
The path to the php.ini file my Laravel Valet uses is /opt/homebrew/etc/php/8.1/php.ini
In order to see changes in phpInfo() calls, the valet restart command is not good enough. I must use the command brew services restart [email protected]
Anytime xDebug examines PHP files, it will create log entries if its configuration specifies a log file location. I found that log entries were created when Valet was starting up but not when pages were loaded in the browser.
The remote_log and remote_log_level settings are commented-out with semicolons because my configuration is now working. A log level value of 10 will grow the log file to hundreds of megabytes in just a few hours of loading pages.
Do not try to prettify the ext-xdebug.ini file by putting spaces around the equals signs. My breakthrough to a working debugger occurred shortly after I removed spaces from ext-xdebug.ini, updated Valet, and restarted my computer.
Contents of VS .code-workspace file
{
"folders": [
{
"path": "."
}
],
"settings": {},
"launch": {
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9001
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"stopOnEntry": true,
"cwd": "${fileDirname}",
"port": 9001,
"runtimeExecutable": "/usr/local/Cellar/php/7.4.9/bin/php",
"pathMappings": {
"/Users/Corey/Sites/sitename": "${workspaceFolder}"
}
}
]
}
}
The runtimeExecutable value points to the PHP executable installed by homebrew. The pathMappings value points to the place where this website lives on my computer.
Stop and restart the debugger in VS Code everytime you change the workspace file or the web server is restarted.
M1 Processor
I got a new Macbook, and restored my old Intel processor back up to it. That means there are two versions of homebrew on my computer now, and I got into a situation where phpInfo() calls on web pages returned a different version of PHP than php -v in terminal. This fixed that, but I do not know why:
This week, I’m altering functions and stored procedures in a SQL Server 2008 database so that it can be migrated to Azure. The Data Migration Assistant does a great job of generating reports identifying MigrationBlockers, but one type of error was vague enough to confuse me for a few minutes. Here’s an example of that error:
{
"Recommendations": [
{
"ApplicableCompatibilityLevels": [
"CompatLevel100",
"CompatLevel110",
"CompatLevel120",
"CompatLevel130",
"CompatLevel140"
],
"ChangeCategory": "MigrationBlocker",
"RuleId": "46010",
"Title": "One or more objects contain statements that are not supported in Azure SQL Database [46010]",
"Impact": "While assessing the schema on the source database, one or more syntax issues were found. Syntax issues on the source database indicate that some objects contain syntax that is unsupported in Azure SQL Database.",
"ImpactDetail": "Function: [dbo].[GalleryXML] contains a statement that is not supported on Microsoft Azure SQL Database v12. The specific error is: Incorrect syntax near AS.",
"Recommendation": "Note that some of these syntax issues may be reported in more detail as separate issues in this assessment. Review the list of objects and issues reported, fix the syntax errors, and re-run assessment before migrating this database.",
"MoreInfo": ""
}
],
"IsSelectedForMigration": true,
"Eligibility": {
"IsEligibleForMigration": true,
"Explanation": "OK"
},
"ObjectName": "GalleryXML",
"SchemaName": "dbo",
"ObjectType": "UserDefinedFunction"
},
The error text, “Incorrect syntax near AS,” was just not a strong clue as to what was wrong with this function. The purpose of the function is to create structured XML that can be de-serialized into an instance of an object in C#. I use PATH Mode to accomplish this, and it is one of my favorite maneuvers when operating in SQL Server/.Net. Here is an abbreviated version of the query to help you understand the required changes to continue using this method in an Azure database:
SELECT
ID,
...
--Subquery for photos
(
SELECT
ID,
...
FROM GalleryPhotosTbl
WHERE GalleryID = GalleryTbl.ID
FOR XML PATH('GalleryPhoto'), TYPE
) AS Photos
FROM GalleryTbl
WHERE GalleryTbl.ID = @ID
FOR XML PATH('Gallery'), TYPE
The syntax that Azure doesn’t support is “AS Photos.” Here is the solution:
SELECT
ID,
...
--Subquery for photos
(
SELECT
ID,
...
FROM GalleryPhotosTbl
WHERE GalleryID = GalleryTbl.ID
FOR XML PATH('GalleryPhoto'), root ('Photos'), TYPE
)
FROM GalleryTbl
WHERE GalleryTbl.ID = @ID
FOR XML PATH('Gallery'), TYPE
I needed to detect a mixed case string in classic ASP. I define a mixed case string as containing both upper and lower case characters, like AuxagGsrLpa. I could not find a free function on the web to make this decision, so I wrote one.
<%
Function isMixedCase( str )
'detects mixed case strings using the english alphabet
'ignores spaces and other non-alphabetic characters
str = trim( str )
isMixed = false
lastCase = ""
for i=1 to len( str )
currentChar = mid( str, i, 1 )
if asc( currentChar ) >= 65 and asc( currentChar ) <= 90 then
if lastCase <> "" and lastCase <> "upper" then
isMixed = true
exit for
else
lastCase = "upper"
end if
else
if asc( currentChar ) >= 97 and asc( currentChar ) <= 122 then
'lower
if lastCase <> "" and lastCase <> "lower" then
isMixed = true
exit for
else
lastCase = "lower"
end if
else
lastCase = ""
end if
end if
next
if isMixed then
isMixedCase = true
else
isMixedCase = false
end if
End Function
%>
Here are a few test strings to demonstrate the functionality:
The goal of this page is to put two MD5 code samples on the same web page, one for ASP classic and one for ASP.NET. String formatting (like ASCII vs UTF-8) can trip up coding these two routines. These two code samples will produce the same MD5 hash output.
Classic ASP/VBScript MD5
The ASP code is simple once you have a copy of md5.asp from http://frez.co.uk. The only file in the ZIP that you need is md5.asp.
The keys to getting the .net output to match are encoding.ascii and toString("x2").
<%@ Import Namespace="System.Security.Cryptography" %>
<script language="VB" runat="server">
Public Sub Page_Load( sender As Object, e As EventArgs )
Dim strPlainText as String = "[email protected]"
dim md5Hasher as MD5 = MD5.create( )
dim result as byte( ) = md5Hasher.computeHash( encoding.ascii.getBytes( strPlainText))
for each singleByte as byte in result
response.write ( singleByte.ToString("x2") )
next
End Sub
</script>
Today, I needed to convert a time stamp like “1/25/2011 10:42:11 AM” to a readable sentence format like “2 months and 6 hours ago.” Here’s the code I came up with:
function timeAgo( byval time )
sentence = ""
hits = 0
piecesAgo = array( 0, 0, 0, 0, 0, 0 )
piecesTotals = array( 0, 12, 30, 24, 60, 60 )
labels = array( "year", "month", "day", "hour", "minute", "second" )
dateAddSymbols = array( "yyyy", "m", "d", "h", "n", "s" )
for p=0 to uBound( piecesAgo )
piecesAgo( p ) = 0 + datediff( dateAddSymbols( p ), time, now( ))
if piecesAgo( p ) <> 0 then
time = dateAdd( dateAddSymbols( p ), piecesAgo( p ), time )
end if
next
'remove negative values
for p=uBound( piecesAgo ) to 0 step -1
if piecesAgo( p ) < 0 and p > 0 then
piecesAgo( p ) = piecesAgo( p ) + piecesTotals( p )
piecesAgo( p-1 ) = piecesAgo( p-1 ) - 1
end if
next
for p=0 to uBound( piecesAgo )
if piecesAgo( p ) <> 0 then
if len( sentence ) > 0 then
sentence = sentence & " and "
end if
sentence = sentence & piecesAgo( p ) & " " & labels( p )
if piecesAgo( p ) > 1 then
sentence = sentence & "s"
end if
hits = hits + 1
if hits >= 2 then
exit for
end if
end if
next
set piecesAgo = nothing
set piecesTotals = nothing
set labels = nothing
set dateAddSymbols = nothing
timeAgo = sentence & " ago"
end function
Providing labels is a great way to help users interact with your website properly. I like to put instructional text inside text boxes to save space. Users get annoyed when the text inside the box they click on does not disappear when they are ready to type. Users do not want to backspace default instructional text before typing in boxes, so some code is required to make this happen.
I would like to share two small Javascript functions. The first clears out default text box contents automatically when a user clicks on the box. The second restores the default instructional text if the user leaves the box empty.
Implement these functions using onfocus=”wash(this);” and onblur=”checkWash(this);” on your text input control. You may want to avoid the second function on text fields where input is optional, so the user can leave the text box blank.
Instead of using the default 500 item in the list, create a new handler for status code 500.100. Point this at your script, and you should be all set to log errors.
Another way is to enter the Error Pages module of the website profile, and click “Edit Feature Settings” on the right hand sidebar.
This screen will appear:
Configure yours in a similar fashion, and Server.GetLastError will start working in your script.
Disguise your email address or any text with this character obfuscation. This code corey@example.com will show up on a web page as [email protected]. You can share your email address without worrying that it will be collected by a spam bot.
Enter some plain text
Some losers send spam email for a living, and will send garbage to any email they can find online. Obfuscating email addresses in character codes cloaks them from some of the leeches. I decided to create code to automate this task.
Here is an ASP classic function that will convert a string to ASCII characters. PHP code below. These characters will display as normal text to the casual user. The difference between alphabet characters and ASCII characters is that encoded characters must be evaluated before they look like an email address. This thin veil of secrecy is enough to fight off some email harvesters.
public function asciiDisguise( string )
build = ""
for i=1 to len( "" & string )
build = build & "&#" & asc( mid( string, i, 1 )) & ";"
next
asciiDisguise = build
end function