
История зарождения PHP
История PHP начинается не с полноценного языка программирования, а с набора CGI-скриптов на C, известного как PHP/FI 1 (Personal Home Page Tools/Forms Interpreter). Позже, когда проект превратился в полноценный интерпретатор скриптов, акроним PHP стал расшифровываться как Hypertext Preprocessor. В этой статье мы возвращаемся к истокам PHP, рассматривая его первую версию, ее компиляцию и функциональность.
Автором PHP является Расмус Лердорф, создавший и выложивший в открытый доступ PHP/FI 1 в июне 1995 года. Возможно, он и не представлял, какое влияние его проект окажет на мир веб-разработки. В то время не было популярных сегодня систем контроля версий и хостингов IT-проектов. До появления git оставалось 10 лет, и концепция открытого ПО не была сформирована в том виде, в котором мы ее знаем сейчас. Поэтому и полноценных сервисов для его распространения не существовало. Впервые автор PHP анонсировал свой проект в списке рассылки Comp.lang.Newsgroups, посвященной веб-разработке.
Изначально проект создавался для собственных нужд с целью упростить управление личным веб-сайтом автора, отслеживать посещаемость, управлять доступом к контенту, обрабатывать результаты веб-форм, сохранять данные и отображать результаты.
Сборка и запуск PHP/FI 1 спустя почти 30 лет
Все версии PHP, включая самые ранние, можно найти на сайте Museum of PHP.
Для начала скачаем самую первую версию и распакуем архив:
wget https://museum.php.net/php1/php-108.tar.gz -P php1 cd php1 tar -xzvf php-108.tar.gz -C .
Посмотрим, какие файлы содержатся в архиве:
Список файлов:
php/phpf.c
php/phpl.c
php/phplview.c
php/phplmon.c
php/common.c
php/error.c
php/post.c
php/wm.c
php/common.h
php/config.h
php/subvar.c
php/html_common.h
php/post.h
php/version.h
php/wm.h
php/Makefile
php/README
php/License
Хоть файлов и немного, но есть README. Однако его содержание сводится к следующему:
Отредактируйте файлы
config.hиMakefileв соответствии с вашей системойВведите команду:
makeСкопируйте CGI-бинарные файлы в вашу директорию CGI. Для большинства сайтов, использующих NCSA HTTPD, вы можете просто назвать их filename.cgi и поместить в вашу директорию ~/public_html.
Я рекомендую запускать эти скрипты под вашим собственным идентификатором пользователя, а не под идентификатором пользователя httpd.
Проверьте http://www.io.org/~rasmus для полных инструкций по установке и настройке. (Да, я ленив. Мне не хочется писать всё дважды.)
Eсли хотите, подпишитесь на список рассылки PHP, отправив электронное письмо на адрес:
majordomo@kajen.malmo.se
со строкой:subscribe php-listyour_email_address
в теле сообщения.
Конечно, сайта http://www.io.org/~rasmus уже не существует, но мы можем следовать инструкциям и настроить проект.
В файле config.h есть следующие директивы препроцессора:
#define ROOTDIR "~/html" // корневая директория #define HTML_DIR "." // папка с html файлами #define LOGDIR "." // папка для лог-файлов #define ACCDIR "." // папка с файлами контроля доступа #define NOACCESS "NoAccess.html" // файл для отображения в случае отсутствия доступа
В Makefile просто укажем сборку в режиме отладки и дебаг-мод для проекта:
... # Generic compiler options CFLAGS = -g -O2 -Wall -DDEBUG $(OPTIONS) #CFLAGS = -O2 $(OPTIONS) ...
Теперь выполним команду make которая скомпилирует 4 CGI программы:
make ... ls | grep ".cgi" phpf.cgi phpl.cgi phplmon.cgi phplview.cgi
На данном этапе неизвестно, за что отвечает каждая из них. Для работы нам понадобится CGI веб-сервер. В 1995 году самым популярным был NCSA HTTPd, который в том же году стал основой для Apache HTTP Server. Однако, сегодня предлагаю написать минимальную реализацию CGI веб-сервера на современном PHP.
CGI Веб-сервер для PHP/FI на современном PHP
Для начала небольшая справка о том, что такое CGI. CGI (Common Gateway Interface) — это стандарт, который позволяет веб-серверу передавать запросы пользователей на обработку внешним программам или скриптам, а затем возвращать результаты обратно пользователю. Это обеспечивает динамическую генерацию веб-страниц и интерактивное взаимодействие с пользователем. Данный стандарт появился в начале 1990-х и был популярен до начала 2000-х годов.
Базовая спецификация CGI:
Механизм Запроса: Веб-сервер идентифицирует CGI-скрипты либо по специально настроенному каталогу (например,
/cgi-bin/), либо по расширению файла (например,.cgiили.pl). Когда сервер получает запрос на такой ресурс, он выполняет скрипт вместо того, чтобы отправлять файл напрямую браузеру.Передача Данных: Данные запроса передаются скрипту через переменные среды (environment variables) и стандартный ввод (stdin), а вывод скрипта (stdout) направляется обратно веб-серверу, который отправляет его пользователю.
Параметры запроса: CGI-скрипты получают информацию о запросе через переменные среды. Некоторые из ключевых переменных:
QUERY_STRING: строка запроса, переданная в URL после знака?.REQUEST_METHOD: метод HTTP запроса (например,GETилиPOST).CONTENT_TYPEиCONTENT_LENGTH: информация о теле запроса, актуальная для методов типа POST.SCRIPT_NAMEиPATH_INFO: информация о том, какой скрипт был вызван, и дополнительный путь, переданный скрипту.
Ввод: Для метода
GET, данные передаются черезQUERY_STRING. ДляPOST— данные запроса читаются из стандартного ввода.Вывод: CGI-скрипт должен сначала отправить заголовки ответа (например,
Content-Type), после чего следует пустая строка, и только потом тело ответа.
Плюсы такого подхода заключаются в простоте: веб-сервер запускает внешнюю программу и отдает клиенту результат как есть, при этом не важно на каком языке программирования написан CGI скрипт.
Основным минусом же является необходимость запуска отдельного пр��цесса для обработки каждого запроса, что является довольно ресурсоемкой операцией.
Реализация на PHP
Вся основная логика сервера будет строиться вокруг функции proc_open, которая запускает внешнюю команду, передавая ей аргументы и переменные среды, и открывает дескрипторы ввода/вывода.
<?php $port = 8080; $address = '127.0.0.1'; /** * Парсим запрос и достаем все необходимые данные для работы * @param $request * @return array */ function parseRequest($request): array { $request = explode("\r\n\r\n", $request); $headers = explode("\r\n", $request[0]); $body = $request[1] ?? ''; $headerLine = array_shift($headers); preg_match('/(GET|POST) (\/\S*) HTTP\/1.\d/', $headerLine, $matches); $method = strtolower($matches[1]) ?? ''; $path = $matches[2] ?? ''; $queryParams = parse_url($path); return [ 'method' => $method, 'path' => $queryParams['path'] ?? '', 'query' => $queryParams['query'] ?? '', 'body' => $body, ]; } /** Обработка запроса, вызов cgi программы с параметрами запроса и отправка ответа клиенту * @param $client * @param $request * @return void */ function handleRequest($client, $request) { $method = $request['method']; $path = $request['path']; $query = $request['query']; $body = $request['body']; // Принимаем только get и post запросы if (!in_array($method, ['get', 'post'])) { echo "Error: Invalid method\n"; socket_write($client, "HTTP/1.1 405 Method Not Allowed\r\n\r\n"); return; } // Переменные среды необходимые для PHP/FI 1 $env = [ 'REQUEST_METHOD' => strtoupper($method), 'QUERY_STRING' => $query, 'CONTENT_TYPE' => 'application/x-www-form-urlencoded', 'CONTENT_LENGTH' => strlen($body) ]; // Подготовка команды и параметров для запуска $cmd = "." . $path; $params = str_replace('+', ' ', $query); // Дескрипторы ввода вывода $descriptors = [ 0 => ['pipe', 'r'], // stdin 1 => ['pipe', 'w'], // stdout 2 => ['pipe', 'w'], // stderr ]; // Запуск процесса $process = proc_open($cmd . ' ' . $params, $descriptors, $pipes, '.', $env); if (is_resource($process)) { // Записываем тело запроса в stdin if($body) { fwrite($pipes[0], $body); } fclose($pipes[0]); $output = stream_get_contents($pipes[1]); $errors = stream_get_contents($pipes[2]); if($errors){ var_dump($errors); } fclose($pipes[1]); fclose($pipes[2]); proc_close($process); // Отправляем клиенту ответ $statusCode = strpos($output, 'Location:') !== false ? '302 Found' : '200 OK'; $response = "HTTP/1.1 {$statusCode}\r\n{$output}"; socket_write($client, $response); } } $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); socket_bind($socket, $address, $port); socket_listen($socket); echo "Сервер запущен http://$address:$port\n"; while (true) { $client = socket_accept($socket); $request = socket_read($client, 1024); $parsedRequest = parseRequest($request); handleRequest($client, $parsedRequest); socket_close($client); } socket_close($socket); ?>
GET-параметры PHP/FI 1 получает не через переменную среды QUERY_STRING, а как параметры запуска программы.
Анализ функциональности и исходного кода
phpl.cgi

phpl.cgi - основная программа которая занимается рендерингом html страниц, контролем доступа и сохранением статистики посещения страниц. Программа ищет запрошенные страницы в каталоге, который был указан в константе HTML_DIR файла config.h, проверяет наличие файла All.acc или одноименного запрошенному файлу с расширением .acc, например, для index.html это index.acc. All.acc имеет больший приоритет, и его настройки не переопределяются другими файлами контроля доступа.
Формат файла немного напоминает правила .htaccess из Apache, на каждой строке указываются правило и его аргументы, с помощью функции сопоставления по маске программа проверяет соблюдается ли данное правило. Код этой функции довольно сложен для восприятия, о чем даже указал сам автор в комментариях:
/* * wild_match has been borrowed from eggdrop 0.9m * by Robey Pointer <robey@annwfn.indstate.edu> * * I've just fixed up the prototype and whitespace to be * more readable. -Ben Eng <ben@achilles.net> */ /* * Yanked out C++ comments -Rasmus Lerdorf <rasmus@io.org> */ /* brand new reg.c -- this one seems to get a fairly consistant 10% gain over the old one. i ripped off the one from IRC servers and edited it to make it work a little better (allows better quoting, and returns a number indicating how closely the string matched, just like the old one). */ #include <stdio.h> #include <sys/types.h> #define tolower(c) \ ( ( ( ( c ) >= 'A' ) && ( ( c ) <= 'Z' ) ) ? ( ( c ) - 'A' + 'a' ) : c ) /* this is the matches( ) function from ircd, spaghetti'd up to be more effecient on masks that don't end with a wildcard ( ie: hostmask matches ). first it does a quick scan of the string in reverse, aborting quickly if any non-wildcard characters don't match. once it comes to the first wildcard, it falls into the reverse-engineered ( by fred1 ) matches function. it's proven to be pretty durn fast. thanks to justin slootsky, who came up with the original idea of running the matches backward ( in slightly more complex form ). i tried to put lots of comments, cos string matching is not my thing, and i had to sit and think for 5 minutes for every line. ( ugh! ) */ int wild_match( const char *ma, const char *na ) { const char *mask = ma, *nask = na, *m = ma, *n = na, *mlast; int close = 0, q = 0, wild = 0; /* take care of null strings ( should never match ) */ if( ( ma == (char *)0 ) || ( na == (char *)0 ) ) return 0; if( ( !*ma ) || ( !*na ) ) return 0; /* find the end of each string */ while( *m ) m++; while( *n ) n++; m--; n--; mlast = m; /* check the match backwards */ /* while: chars are identical OR the mask char is an unquoted '?' & haven't reached the start of either string & mask char isn't an unquoted '*' */ while( ( ( tolower( *m ) == tolower( *n ) ) || ( ( *m == '?' ) && ( m[ -1 ] != '\\' ) ) ) && ( m != ma ) && ( n != na ) && !( ( *m == '*' ) && ( m[ -1 ] != '\\' ) ) ) { if( !( ( *m == '?' ) && ( m[ -1 ] != '\\' ) ) ) close++; /* 1 more exact match */ m--; n--; if( *m == '\\' ) m--; /* if mask was quoting something, skip the \ */ } if( ( *m != '*' ) || ( *( m - 1 ) == '\\' ) ) { /* case II - entire string matched, there were no '*' */ if( ( m == ma ) && ( n == na ) && ( tolower( *m ) == tolower( *n ) ) ) return close+1; /* case III - one of the strings ended prematurely ( no match ) */ if( ( m == ma ) || ( n == na ) ) return 0; /* case IV - failed to match the ending strings, so abort */ return 0; } /* else */ /* case I - hit an unquoted '*' -- fall into matching algorithm */ while( 1 ) { q = ( m > mask ) ? ( *( m - 1 ) == '\\' ) : 0; /* quoted? */ if( m < mask ) { if( n < nask ) return close + 1; /* Made it through both strings! */ for( m++; ( m < mlast ) ? ( *m == '?' ) : 0; m++ ) ; /* skip ?s */ if( ( *m == '*' ) && ( m < mlast ) ) return close + 1; if( wild ) { m = ma; n = --na; } else return 0; } else if( n < nask ) { while( ( *m == '*' ) && ( m >= mask ) ) m--; return ( m < mask ) ? close+1 : 0; } q = ( m > mask ) ? ( *( m - 1 ) == '\\' ) : 0; /* quoted? */ if( ( *m == '*' ) && ( !q ) ) { /* unquoted '*' ? */ while( ( m > mask ) ? ( *m == '*' ) : 0 ) m--; /* Throw away the *s */ if( *m == '\\' ) { m++; q = 1; } /* First non-* was quoted, so keep it */ wild = 1; ma = m; na = n; } if( ( tolower( *m ) != tolower( *n ) ) && ( ( *m != '?' ) || q ) ) { /* non-match */ if( wild ) { m = ma; n = --na; q=( m > mask ) ? ( *( m - 1 ) == '\\' ) : 0; /* quoted? */ } else return 0; } else { if( ( *m != '?' ) || q ) close++; /* Unquoted ?s aren't counted */ if( *m ) m--; /* This char got matched */ if( *n ) n--; /* This char got matched */ if( q ) m--; /* This quote went with the char we matched */ } } }
Вот как это работает:
Проверяются случаи, когда одна из строк или обе пустые (не могут совпадать).
Быстро сканирует строку в обратном порядке, прекращая работу, если какие-либо символы, не являющиеся метасимволами, не совпадают.
Пока символы в обеих строках совпадают или символ в шаблоне равен
?(если?не экранирован), и не достигнуто начало обеих строк, и символ в шаблоне не является*.Если символ в шаблоне не является неэкранированным
?, увеличивается счетчикcloseдля точного совпадения.Проверяется, является ли текущий символ в шаблоне экранированным.
В зависимости от условий сравнения возвращаются различные значения:
Если строка совпала полностью без символов
*, возвращаетсяclose + 1.Если одна из строк закончилась до того, как все символы сравнились, возвращается
0.Если не удалось сравнить конечные строки, возвращается
0.
Если обнаружен неэкранированный символ
*, выполняется более сложное сравнение:Проверяется, является ли предыдущий символ экранированным.
Если строка закончилась, считается, что она совпала с пустой строкой.
Пропускаются символы
?.Проверяется, есть ли после
*другой символ в шаблоне, чтобы избежать бесконечного цикла.Если встречается
*, который не экранирован, обрабатывается следующая итерация.Если обнаружен неэкранированный
*, устанавливается флагwild.Проверяется совпадение символов с учетом экранирования.
Если символы не совпадают и
wildустановлен, выполняется возврат к предыдущему*.Если символы совпадают, увеличивается
closeи продолжается сравнение.
wild - фл��г, указывающий на то, что в шаблоне был обнаружен неэкранированный символ *. Когда wild установлен в 1, это означает, что текущее сравнение выполняется после обнаружения *, и программа должна искать совпадения, пропуская определенное количество символов в строке для сравнения.
close - счетчик, отслеживающий количество точных совпадений символов между шаблоном и строкой. Этот счетчик увеличивается только при точном совпадении символов, то есть когда символы в шаблоне и строке совпадают, или когда символ в шаблоне является неэкранированным ?.
Функция возвращает 0 в случае если строки не совпадают, значения больше 0 указывают на количество точных совпадений (без учета символов * и ?).
Не менее запутанным кажется код который обрабатывает сами правила:
/* * CheckAccess * * Returns -1 if access file wasn't found * 0 if user is allowed access * 1 if user is denied access * 2 if user is allowed access but should not be logged */ int CheckAccess(char *fn, int *email_req, int *password_req) { FILE *fp; char *r,*s,*ss=NULL,*t; char buf[128]; char filename[1024]; int Access=0; int Match=0; int DefAccess=0; /* Default is to allow if not specified */ int old_em=0; int old_pw=0; #if DEBUG fprintf(fperr,"In CheckAccess: NoInfo=%d\n",NoInfo); fflush(fperr); #endif strcpy(filename,fn); s=strrchr(filename,'.'); if(s) *s='\0'; if((s=strrchr(filename,'/'))) s++; else s=filename; if(ACCDIR[0]!='/') FixFilename(ACCDIR,buf); else strcpy(buf,ACCDIR); ss=buf+strlen(buf)-1; if(*ss!='/') strcat(buf,"/"); strcat(buf,s); strcat(buf,".acc"); #if DEBUG fprintf(fperr,"Checking access file [%s]\n",buf); fflush(fperr); #endif fp=fopen(buf,"r"); if(!fp) return(-1); while(!feof(fp)) { Match=0; Access=0; if(fgets(buf,127,fp)) { if(buf[0]=='#') continue; /* ignore comments */ s=strchr(buf,' '); /* accept both space and tab as separator */ t=strchr(buf,'\t'); if(!s && t) s=t; if(s && t && t<s) t=s; if(!s) continue; *s='\0'; if(!strcasecmp(buf,"public")) { DefAccess=0; continue; } if(!strcasecmp(buf,"private")) { DefAccess=1; continue; } if(!strcasecmp(buf,"allow")) Access=0; else if(!strcasecmp(buf,"ban")) Access=1; else if(!strcasecmp(buf,"nolog")) Access=2; else if(!strcasecmp(buf,"noinfo") && NoInfo != 2) NoInfo=1; else if(!strcasecmp(buf,"info")) NoInfo+=3; else if(!strcasecmp(buf,"passwordform")) { strcpy(PasswordForm,s+1); t=PasswordForm+strlen(PasswordForm)-1; if(*t=='\n') *t='\0'; #if DEBUG fprintf(fperr,"PasswordForm=[%s]\n",PasswordForm); fflush(fperr); #endif } else if(!strcasecmp(buf,"emailform")) { strcpy(EmailForm,s+1); t=EmailForm+strlen(EmailForm)-1; if(*t=='\n') *t='\0'; #if DEBUG fprintf(fperr,"EmailForm=[%s]\n",EmailForm); fflush(fperr); #endif } else if(!strcasecmp(buf,"email")&&*email_req==0) { *email_req=1; } else if(!strcasecmp(buf,"noemail")) { old_em=*email_req; *email_req=4; } else if(!strcasecmp(buf,"password")&&*password_req==0) { r=s+1; while(*r==' ' || *r=='\t') r++; if(strlen(r)==0) continue; s=strchr(r,' '); /* accept both space and tab as separator */ t=strchr(r,'\t'); if(!s && t) s=t; if(s && t && t<s) t=s; if(!s) continue; *s='\0'; strcpy(acc_password,r); *password_req=1; } else if(!strcasecmp(buf,"nopassword")) { old_pw=*password_req; *password_req=4; } r=s+1; /* Strip leading spaces/tabs */ while(*r==' ' || *r=='\t') r++; if(strlen(r)==0) continue; s=r; s+=strlen(r)-1; while(*s==' ' || *s=='\t' || *s=='\n' || *s=='\r') s--; *(s+1)='\0'; if(!strncasecmp(r,"mail:",5)) { r+=5; if(strlen(r)==0) continue; if(email_addr && strlen(email_addr)) strcpy(filename,email_addr); else continue; } else if(!strncasecmp(r,"browser:",8)) { r+=8; if(strlen(r)==0) continue; if(agent && strlen(agent)) strcpy(filename,agent); else continue; } else if(!strncasecmp(r,"referer:",8)) { r+=8; if(strlen(r)==0) continue; if(referer && strlen(referer)) strcpy(filename,referer); else continue; } else sprintf(filename,"%s@%s",user,host); #if DEBUG fprintf(fperr,"Matching [%s] [%s]\n",filename,r); fflush(fperr); #endif if(wild_match(r,filename)!=0) { #if DEBUG fprintf(fperr,"Got Match\n"); fflush(fperr); #endif if(*email_req==1) { *email_req=2; continue; } if(*email_req==4) { *email_req=3; continue; } if(*password_req==1) { *password_req=2; continue; } if(*password_req==4) { *password_req=3; continue; } if(NoInfo!=1 && NoInfo!=3 && NoInfo!=5) { Match=1; break; } else if(NoInfo==1) NoInfo=2; else NoInfo=0; } else { if(NoInfo==1 || NoInfo==3) NoInfo=0; if(*password_req==1) *password_req=0; if(*password_req==4) *password_req=old_pw; if(*email_req==1) *email_req=0; if(*email_req==4) *email_req=old_em; #if DEBUG fprintf(fperr,"No Match\n"); fflush(fperr); #endif } } } fclose(fp); #if DEBUG fprintf(fperr,"Leaving CheckAccess: NoInfo=%d em=%d pw=%d\n",NoInfo,*email_req,*password_req); fflush(fperr); #endif if(Match) return(Access); else return(DefAccess); }
Общий алгоритм функции таков:
Построчно считывается содержимое файла и обрабатывается каждая строка
Если строка начинается с символа
#, она считается комментарием и игнорируется.Иначе строка разделяется на две части: ключевое слово и значение. Если значение содержит пробелы или табуляции, они удаляются.
В зависимости от ключевого слова, устанавливается определенное правило доступа:
public- доступ разрешен всем.private- доступ запрещен всем.allow- пользователю разрешен доступ.ban- пользователю запрещен доступ.nolog- отключает логирование посещения.noinfo- отключает показ статистики для пользователя.info- разрешает показ статистики для пользователяpasswordform,emailform- определяют кастомные формы для ввода пароля и email соответственно.email- требуется email для доступа.noemail- email не требуется.password- требуется пароль для доступа.nopassword- пароль не требуется.
Если строка начинается с
mail:,browser:,referer:, то значение используется для сравнения с определенной информацией (email, браузер, ссылка), чтобы установить совпадение.
В качестве значения может быть предоставлена маска которая обрабатывается функцией wild_match. Если указать email * то подойдет любая строка
Попробуем составить конфигурацию которая будет запрашивать e-mail и пароль для доступа к странице:
protected.acc:
private email * password secret *
protected.html:
<center> <!-- Hello $email! --> <br> This is protected page! <br> </center>
Результат:


В html файле могут быть обработаны некоторые перменные общий синтаксис таков:
<!-- $var -->
Значения переменных могут быть подставлены из данных post запроса. Также есть несколько "системных" переменных - (cnt, todays_cnt, lasttime, lastuser, modtime, VERSION, email_addr, referer, agent, short_agent, raw_file) они в основном используются для отображения статистики в футере страницы.
Также в качестве аргументов можно передать параметр env или version

phpf.cgi
phpf.cgi - программа отвечающая за обработку данных форм, их сохранение и отображение.
Для записи данных первым аргументом необходимо передать название формы, вторым, опционально, - адрес перенаправления клиента после сохранения данных, например:
phpf.cgi form_name phpl.cgi?index.html
Создадим простую страничку с формой ввода имени и сообщения:
<!DOCTYPE html> <html> <head> <title>Feedback Form</title> </head> <body> <h1>Feedback Form</h1> <form method="post" action="phpf.cgi?form_name+phpl.cgi?index.html"> <label for="name">Name:</label> <input type="text" id="name" name="name" required><br><br> <label for="comment">Comment:</label><br> <textarea id="comment" name="comment" rows="4" cols="50" required></textarea><br><br> <input type="submit" value="Submit"> </form> </body> </html>

Результаты формы будут записаны в .res файл в папке forms.
Для того чтобы прочитать данные формы нужно передать аргументы вида show filename xField, для нашей формы это может быть:

Перед именем поля указывается префикс определяющий форматирование, возможные варианты:
m/M - для e-mail
l/L - для ссылок
t/T - обычный текст
i/I - курсив
b/B - жирный текст
Фрагмент кода отвечающий за форматирование:
void PrintFormType(int type, char *field, char *text) { switch(type) { case 'M': if(field) printf("<b>%s:</b> <a href=\"mailto:%s\">%s</a><br>\n",field,text,text); else printf("<a href=\"mailto:%s\">%s</a><br>\n",text,text); break; case 'm': if(field) printf("<b>%s:</b> <a href=\"mailto:%s\">%s</a> \n",field,text,text); else printf("<a href=\"mailto:%s\">%s</a> \n",text,text); break; case 'L': if(field) printf("<b>%s:</b> <a href=\"%s\">%s</a><br>\n",field,text,text); else printf("<a href=\"%s\">%s</a><br>\n",text,text); break; case 'l': if(field) printf("<b>%s:</b> <a href=\"%s\">%s</a> \n",field,text,text); else printf("<a href=\"%s\">%s</a> \n",text,text); break; case 'T': if(field) printf("<b>%s:</b> %s<br>",field,text); else printf("%s<br>",text); break; case 't': if(field) printf("<b>%s:</b> %s ",field,text); else printf("%s ",text); break; case 'I': if(field) printf("<b>%s:</b> <i>%s</i><br>",field,text); else printf("<i>%s</i><br>",text); break; case 'i': if(field) printf("<b>%s:</b> <i>%s</i> ",field,text); else printf("<i>%s</i> ",text); break; case 'B': if(field) printf("<b>%s:</b> <b>%s</b><br>",field,text); else printf("<b>%s</b><br>",text); break; case 'b': if(field) printf("<b>%s:</b> <b>%s</b> ",field,text); else printf("<b>%s</b> ",text); break; } }
phplmon.cgi
Программа для обработки логов и отображения статистики посещения страниц. Ее функциональность довольно проста: она анализирует файлы .cnt, которые создает phpl.cgi, и выводит результат в табличном виде - имя страницы, всего посещений, посещений за день, дата последнего посещения и кем была посещена страница.

phplview.cgi
Предоставляет более подробную статистику по каждому посещению страницы, данные берутся из файлов .log которые также пишет phpl.cgi.

Заключение
Такой была самая первая версия PHP выложенная в публичный доступ в 1995 году. Небольшой набор инструментов для контроля доступа, ведения статистики посещений и сохранения данных веб-форм.
Этот фрагмент интервью Расмуса Лердорфа, опубликованный Кевином Янком на сайте SitePoint в 2002 году, раскрывает интересные аспекты развития PHP и его отличия от других языков.
SP: Что привело вас к разработке PHP? И что, по вашему мнению, отличает этот язык от других?
RL: Первая версия PHP была простым набором инструментов, которые я создал для своего веб-сайта и нескольких других проектов. Один инструмент записывал статистику посещений в базу данных mSQL, другой интерпретировал данные формы. В итоге у меня было около 30 небольших CGI-программ, написанных на C, прежде чем мне это надоело, и я объединил их все в одну библиотеку на C. Затем я написал очень простой парсер, который извлекал теги из HTML-файлов и заменял их результатом соответствующих функций в библиотеке на C.
Этот простой парсер постепенно стал включать условные теги, затем циклы, функции и т. д. Ни в один момент я не думал, что пишу язык сценариев. Я просто добавлял немного функциональности к парсеру макросов. Всю свою реальную бизнес-логику я все еще писал на C.
В конце концов, то, что, по моему мнению, выделяло PHP на ранних этапах и до сих пор выделяет его среди других, это то, что он всегда стремится найти самый короткий путь к решению проблемы задач веба. Он не пытается быть языком общего назначения, и любой, кто ищет решение веб-задачи, обычно найдет очень прямое решение через PHP. Многие альтернативы, которые утверждают, что решают задачи веба, просто слишком сложны. Когда вам нужно, чтобы что-то заработало к пятнице, чтобы вы не провели всё выходные перелистывая 800-страничные руководства, PHP начинает выглядеть довольно хорошо.
SP: Какое решение, по вашему мнению, было самым важным за годы разработки PHP? Есть ли какие-либо решения, которые вы приняли, и теперь вам хотелось бы, чтобы вы приняли их по-другому?
RL: Мне трудно пересмотреть решения, которые были приняты 6 или 7 лет назад, когда PHP использовался всего одним человеком. Не забывайте, что я не сел писать скриптовый язык, который бы использовали 9 миллионов доменов: я сел решить проблему. Решение проблемы к 17:00, чтобы вы могли пойти в кино со своей девушкой, приводит к некоторым аспектам, которые не идеальны 7 лет спустя, когда тысячам людей приходится работать над тем хаком, который вы добавили поздней ночью.
Самое важное решение, которое я принял, вероятно, это было отказаться от контроля. Открыть проект и дать почти всем, кто просил, полный доступ к исходным кодам PHP. Это привело к появлению множества отличных талантов, и люди обычно чувствовали реальное чувство причастности. Проект PHP, вероятно, один из самых крупных по числу людей с доступом к репозиторию CVS, где находится код и документация.
Настроенный проект с сервером доступен для изучения на github.
