Revive PHP Tools — A journey to the ‘90s

Image for post
Image for post
I took this historically accurate screenshot using Windows 10 and Docker…

TL;DR; If you want to try it out, clone my repo.
Notable mentions: /u/cytopia (I found his comment after writing this)

Warm up

This is a guide and my story on how to kickstart the oldest working PHP source code but failing to put in its natural habitat.

In the middle of the ’18 summer I saw a link on Hacker News to the oldest PHP version. Maybe it was /r/PHP. I’m not sure but that’s not really the point. Since college I kind of settled on backend development and since pursue a career in it. So seeing PHP 1.0 source with my own eyes meant only one thing. I have to run it.

Image for post
Image for post
Museum is a good name for such a place. Old and weird things.

History time

After all what is PHP 1.0? Where did it come from? Were there any alternatives? (Short and correct answer here)

We are talking about 1995. Technically there weren’t any server-side programming languages at that time. People used what they already had. C (example), Perl, Bash, to name a few. These languages were extended by libraries to make it easier to process http requests but they weren’t made for the web, like PHP and JavaScript today.

So how do you create a backend service? Back in the day we used something called CGI scripts. Let’s say I have an Apache server and a cgi-bin directory in it. If I call
then located in said folder will be called with the respective arguments. It’s easy, versatile and absolutely language agnostic. (shebang, executable flag, etc.)

You already know why I’m talking about CGI. PHP also needs that to run. When you know nothing about the technology, like me, it is pretty hard to configure it. Apache barely gives you a hint on why the script is failing. Instead, the actual CGI script needs to tell you somehow why it failed during invocation. If the implementation is barely logging something you’re in for a long debugging session.

Most of the websites worked this way: You have some static html pages and they all link to /cgi-bin/something for processing your input or to show dynamic content.

So in 1995, Rasmus Lerdorf wrote a not so long (4 kloc) C program that he named ‘Personal Home Page Tools’. He needed those tools because every other ‘tool’ he could have was too verbose (like JSP) and he only needed some minor scripting for the complexity of a personal website. In June 8, 1995 the deed was done, PHP was born. But we should not forget that at the time the closest equivalent to PHP is the current Mustache templating engine. Yes, you heard it right. Variable substitution, and… that’s it. Have fun writing Facebook in PHP 1.0.8.

Analyze the tar file

Let’s cut to the point. I’m kind of a Windows guy so I just exported the content with 7Zip. I did not know what to expect. Is it gonna be something old and specific that I can’t even run? Should I prepare a Linux distro from ’95 to run this beast? No. All you need is libc and gcc. After all the source code is really simple. This is the content of the archive:

#: ls -1 | paste -sd " " -
License Makefile README common.c common.h config.h error.c html_common.h phpf.c phpl.c phplmon.c phplview.c post.c post.h subvar.c version.h wm.c wm.h

Nothing fancy. Makefile suggests I should type make. (I installed gcc on latest Ubuntu (WSL2) to compile the project)

#: make
gcc -O2 -DFLOCK -c -o phpl.o phpl.c
phpl.c: In function ‘LogEntry’:
phpl.c:182:3: warning: implicit declaration of function ‘flock’; did you mean ‘clock’? [-Wimplicit-function-declaration]
flock(fileno(fp),LOCK_EX); /* Lock file */

This goes on for a hundred lines and looks very suspicious. I check the Makefile. Turns out the source is full of preprocessor macros so I need to modify the Makefile to set the right constants. Since the comments state that flock() is a BSD specific function and lockf() is for Linux, I need to add the
-DLOCKF parameter to change it. Oh and by the way CFLAGS was not added to the compilation so I had to do that too. It’s a shame that I had to tinker so much with the Makefile even though this was a functional version 25 years ago. I guess? Oh and DEBUG is also a nice constant to add to see some error logs when PHP is called.

I ran make again. This time no warnings. The output was 4 binaries:
phpf.cgi, phpl.cgi, phplmon.cgi, phplview.cgi
These are the binaries that can be called by the following URL:*.cgi?arg1+arg2….

The output explains why PHP was called Personal HomePage Tools. We get 4 binaries for 4 different tasks. I later explain what those tasks are.

First run

Now that I have the binaries, I wanted to try them out in a real environment. Here’s the Dockerfile I used:

# Use 'latest' instead of 'alpine'.
# Alpine uses a different libc version
# and you would need to x-compile for it
# which is too much effort.
FROM httpd:latest
# Convenience tools for on-the-fly configuration
RUN apt-get update
RUN apt-get install bash
RUN apt-get install vim -y
# Init Apache2
# Copy config where CGI module is enabled
COPY httpd.conf /usr/local/apache2/conf/httpd.conf
# Create log file for Apache
RUN touch /var/log/httpd
# Copy CGI files
COPY php/cgi-bin/* /usr/local/apache2/cgi-bin
# Create log folder for PHP
RUN mkdir /usr/local/apache2/cgi-bin/log
# Copy test html files
COPY display.phtml index.html /usr/local/apache2/htdocs

At this point I did not bother with delving into the source code so I just ran it and see if I can make it show me something.

Image for post
Image for post
All my files are missing. I might need to edit the path prefixes.

No luck. The error said that the error log cannot be created. Turns out I do have to modify the source code a bit to start this up.

The codebase

Now that we have to check and potentially repair the code to move forward, I might just tell you all about the features of this version in one breath.

Well, let’s just say that this is as ugly as it can get. Given the lack of features this version (remember, it’s only 1.0.8) has, it is safe to say that the project at this stage is meant to serve the author only.

The file structure is flat. Headers and source files thrown in the same directory.

Contains ROOTDIR, HTML_DIR, ACCDIR, LOGDIR, NOACCESS constants. After some debugging, I realized I can’t just set them to an absolute path. The actual hierarchy is as follows:

ROOTDIR (/usr/local/apache2)
* HTML_DIR (htdocs)
* ACCDIR (logs/)
NOACCESS (NoAccess.html)
LOGDIR (/usr/local/apache2/cgi-bin/log)

To make things worse, NOACCESS is not even used. If access is denied, ShowFile(NOACCESS,…) shows something specific instead of the NoAccess.html page.

…What’s missing from the config.h file though, is the MSQL flag (can be found in subvar.c) which enables SQL queries to be executed with Mini SQL. msql usage would be an interesting adventure but for us it is cut short short by the lack of legacy debian packages. I found the msql library but not the corresponding C api library so it was a worthless Wayback Machine ride.

The MSQL support would make it possible to write the following PHP expression:

<!--sql websiteDb select * from users where name='$username'-->

Sadly the whole MSQL business is just an unfinished block of code “commented out” by an #if MSQL. In this version you can barely find meaning in the database query related logic. Later, version 1.99 used a more familiar style: (but we won’t talk about it)

<?$result = msql("test","select * from test");
echo msql_result($result,0,"name")>

Every CGI executable has a main() function just like in any standard console application. (This makes it possible to use PHP in terminal which makes testing a breeze. If it had any tests. Ha.)

Every executable has an error logging hidden behind the DEBUG flag. Logging happens in a verbose manner. Program starting, values, entering condition, etc. The first modification I had to perform was to change the relative error logging to absolute.

fperr=fopen(LOGDIR "/" "phpl.err","w");

The author forgot to add LOGDIR to the fopen call and I was greeted by a runtime error so I had to fix it.

Every executable has the following parameters and shows the same response:

/cgi-bin/php{f,l,lmon,lview}.cgi?version: Shows the version page. Not too useful nowadays.

/cgi-bin/php{f,l,lmon,lview}.cgi?env: Shows the environment variables injected for the executable by the CGI module.

Image for post
Image for post
The form tool of PHP Tools

I start with the hardest tool to present. Let’s see. It wants us to call the executable with form+{some kind of resource url} as the argument.

/cgi-bin/phpf.cgi?form+debug gives us the form parameters that we sent in the request. Plus it gives us all the env values for a good measure.

Image for post
Image for post
Result of form+debug

/cgi-bin/phpf.cgi?form+dest_url+result_path: Write form data to result_path (*.res) and redirect to dest_url.

/cgi-bin/phpf.cgi?dynamic+form_path: Load form file and show it to the user. I honestly could not decipher it.

/cgi-bin/phpf.cgi?show+form_path: Shows the *.res file that holds all the responses to a given form so far.

Technically this is what later become PHP as we now know it. Reads a file and parses it. The ‘L’ might refer to language? I’m not sure. The full feature list is as follows:

  • Can read *.acc file that sets access for the html file with the same name
  • Substitute POST parameter with: <!--$param-->
    Yes, (the author of) PHP didn’t know HTML would use <!-- later as a comment block so it was just natural to use it for substitution.
  • Substitute generic parameter with actual value: <!--$today--> → 27 (users viewed the page today)
  • Run terminal command with: <!--!COMMAND--> (can be disabled with the preprocessor constant NOSYSTEM)


Image for post
Image for post
Example form (source code below)
<title>Parsing Example</title>
<p>Some text. <em>&#42;wow&#42;</em></p>
<FORM ACTION="/cgi-bin/phpl.cgi?display.phtml" METHOD=POST>
<INPUT TYPE="text" name="name">
<INPUT TYPE="text" name="age">
<INPUT TYPE="submit">
Image for post
Image for post
Example result (source code below)
<p><!--$name--> you are <!--$age--> years old!
<!--!cat /etc/passwd-->

Shows this page:

Image for post
Image for post

It can also update cross-references between the pages.

File stat viewer. Speaks for itself. Accessing the following URL loads this table:

Image for post
Image for post


I spent a whole day with this project but I had no energy to write a decent article about it. The original environment I wanted to put PHP in is Debian 0.91 from 1995 [link]. Sadly I could not get networking to work after many trial and failure. So I stuck with Docker. I was also planning to host it in AWS for fun but CGI and PHP 1.0 sounds so unsecure that I wouldn’t even let it run in the safest docker container. This was all I wanted to tell you about PHP 1.0.8. I hope you had fun reading it.

Unrelated remarks

I don’t want to hijack the topic of this article but I absolutely hate it that low-level programmers have the nerves to call a variable

  • buf instead of formResponseFilePath
  • divi instead of dividerIndex
  • s instead of formResponseContent
  • fp instead of formResponseFileHandler

I mean worst case you have a 31 character limitation but it’s not hard to avoid. I don’t want to comment this, funny though:

} /* while */
} /* ScanRefs */

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store