Class InstallWizard

Description

class for performing installation tasks

Overview

Dialog screens

The installer basically consists of some six dialog screens where the user is supposed to enter some data (e.g. language, install type, etc.). Each of those screens has a [Next] button and most screens have a [Previous] button. Also, every screen has a [Cancel] button. The [Cancel] button always immediately leads to the Cancel screen and the whole process is stopped (by resetting the collected information in $_SESSION['INSTALL']). The [Next] button always validates/processes the data the user entered. If the results are good, we go to the next step. If the processing fails, we stay where we are (ie. the current dialog is re-displayed). The [Previous] button backs up one step, without saving or storing the user entered data; the user MUST press [Next] to save the data entered.

The Finish-dialog

The FINISH-dialog has no [Previous] button, because all the real work is done when the user presses [Next] in the CONFIRM-dialog. This is a one-time action (creating tables, filling with demodata, etc.) so it makes no sense to backup at that point.

The stage variable and backing up via the menu

The variable 'stage' moves along with the highest dialog the user has successfully reached. This variable is responsible for greying out/disabling the menu options. The menu can be used to jump back a few steps in the procedure. However, once the transition from CONFIRM to FINISH is made, it is no longer possible to return to previous steps (it makes no sense to do so because at that point the real work is already done). The jump to a particular step/dialog is done via the GET-parameter 'step'. The buttons all work via the POST'ed parameter dialog.

Special cases

There are a few special cases:

  • download: this yields an immediate download of the constructed config.php and no further dialog is displayed
  • done: this is a pseudo-dialog. In effect it is a redirect to the newly created site (either admin.php or index.php or perhaps manual.php.

Located in /program/install.php (line 112)


	
			
Variable Summary
 string $license
 string $messages
 array $results
 string $time_start
Method Summary
 InstallWizard InstallWizard ()
 string appropriate_legal_notices ([bool $high_visibility = FALSE], [string $m = ''])
 string button (string $button)
 bool check_for_nameclash (bool $demodata, string $label, string $username)
 bool check_license ()
 void clamscan_installed (string &$clamscan_path, string &$clamscan_version)
 bool create_tables (string $filename)
 double diff_microtime (string $time_start, string $time_stop)
 void errorcount_bump ()
 int fetch_license (string &$license)
 bool gd_supported (string &$details)
 array get_manifests (string $path)
 string get_menu (int $dialog, int $stage, [string $m = ''])
 string get_page ([string $dialog_title = ''], [string $menu = ''], [string $content = ''], [string $help_topic = 'install'])
 mixed get_utf8_parameter_string (array &$gpc, string $name, [mixed $default_value = ''], [mixed $error_value = ''], [mixed $maximum_length = 255])
 array guess_url ()
 bool insert_tabledata (string $filename)
 string invisible_test_image ([string $m = ''])
 string magic_unquote (string $value)
 string microtime ()
 int|bool mysql_close (string $db_type, string $db_link)
 resource|object|bool mysql_connect (string $db_type, string $server, string $username, string $password)
 string|bool mysql_get_server_info (string $db_type, string $db_link)
 int|bool mysql_num_rows (string $db_type, string $result)
 resource|object|bool mysql_query (string $db_type, string $db_link, string $query)
 string|bool mysql_real_escape_string (string $db_type, string $db_link, string $str)
 string|bool mysql_select_db (string $db_type, string $db_link, string $db_name)
 string|bool mysql_set_charset (string $db_type, string $db_link, string $charset)
 string mysql_utf8mb3 (string $utf8str)
 int|bool mysql_utf8_support (string $db_type, resource $db_link)
 void quasi_random_string (int $length, [int $candidates = 36])
 string render_dialog (array $dialogdef, string $m)
 void run ()
 string sanitise_filename (string $filename)
 bool save_cms ()
 bool save_database ()
 bool save_language ()
 bool save_user ()
 void show_dialog_cancelled ([string $m = ''])
 void show_dialog_cms ([string $m = ''])
 void show_dialog_compatibility ([string $m = ''])
 void show_dialog_confirm ([string $m = ''])
 void show_dialog_database ([string $m = ''])
 void show_dialog_finish ([string $m = ''])
 void show_dialog_installtype ([string $m = ''])
 void show_dialog_language ([string $m = ''])
 void show_dialog_license ([string $m = ''])
 void show_dialog_user ([string $m = ''])
 string t (string $key, [array $replace = ''], [string $lang = ''])
 TRUE validate (array $item, string $value)
 TRUE validate_password (string $label, string $password, [int $min_lower = 1], [int $min_upper = 1], [int $min_digit = 1])
Variables
string $license (line 120)
  • var: $license ready-to-use HTML-code with the text of the license from /program/license.html
string $messages = array() (line 114)
  • var: $messages collects error messages if any
array $results = array() (line 117)
  • var: $results collects outcome of various compatibility results in human readable form
string $time_start (line 123)
  • var: $time_start keeps track of execution time of this script
Methods
Constructor InstallWizard (line 138)

constructor

this constructs the install wizard and also makes sure that the INSTALL-array (kept in the $_SESSION array) is initialised with default values if it did not already exist.

Also we check if the user wants to set or change the debug value. If set to non-0, we show extra information lateron (and also set the parameter in $CFG in config.php.

Finally, we do record the microtime as early as possible, to calculate elapsed time after we are all done.

InstallWizard InstallWizard ()
appropriate_legal_notices (line 2543)

construct a link to appropriate legal notices as per AGPLv3 section 5

This routine constructs ready-to-use HTML-code for a link to the Appropriate Legal Notices, which are to be found in /program/about.html. Depending on the highvisibility flag we either generate a text-based link or a clickabel image.

The actual text / image to use depends on the global constant WAS_ORIGINAL. This constant is defined in /program/version.php and it should be TRUE for the original version of Website@School and FALSE for modified versions.

In the former case the anchor looks like 'Powered by Website@School', in the latter case it will look like 'Based on Website@School', which is in line with the requirements from the license agreement for Website@School, see /program/license.html.

IMPORTANT NOTE

Please respect the license agreement and change the definition of WAS_ORIGINAL to FALSE if you modify this program (see /program/version.php). You also should change the file '/program/about.html' and add a 'prominent notice' of your modifications.

Note: a comparable routine can be found in waslib.php.

  • return: ready-to-use HTML
string appropriate_legal_notices ([bool $high_visibility = FALSE], [string $m = ''])
  • bool $high_visibility: if TRUE we return a text-only link, otherwise a clickable image
  • string $m: margin to improve readability of generated code
button (line 2821)

shorthand for creating a submit button in the correct style

  • return: ready-to-use HTML-code for a submit-button
string button (string $button)
  • string $button: indicates which button to create, e.g. 'next', 'previous', 'cancel', 'finish', 'ok'.
check_compatibility (line 1317)

check certain compatibility issues and optionally return test results

this routine performs a few tests and returns an overall go/nogo signal Human readable test results are stored in $this-results. Return TRUE on passing all tests, FALSE otherwise + errors in $this->messages

  • return: TRUE if all tests passed, FALSE otherwise + information in $this->messages
  • todo: add more tests, e.g. for gd, safe_mode, memory limit, etc.
bool check_compatibility ()
check_for_nameclash (line 3251)

check for name clash of new user (webmaster) and user accounts from demodata

This routine checks to see if the name the webmaster supplied is not one of the demodata accounts. If so, we flag the error in the messages.

Note: The list of accounts must be updated whenever the demodata is updated. This is a kludge but I'll leave it this way for the time being. See also the main file demodata.php.

  • return: TRUE if there is no name clash, FALSE + message in $this->messages otherwise
bool check_for_nameclash (bool $demodata, string $label, string $username)
  • bool $demodata: is installation of demodata requested?
  • string $label: name of the username field in dialog
  • string $username: the proposed username for the webmaster account
check_license (line 506)

check if the user accepts the licences

This is the companion routine for show_dialog_license(). It checks whether the user dutyfully typed 'I agree'. Note that we check caseINsensitive, so 'i agree' is acceptable, as is 'I aGrEe'. Also note that we accept non-ascii UTF-8, say "J'accept'ee" or "J'ACCEPT'EE" which has an e-acute. Case folding to lowercase accepts all those variants.

  • return: TRUE on success, otherwise FALSE + error information in $this->messages array
bool check_license ()
check_validation (line 1568)

shorthand to check the validation status of the relevant dialogs

this checks the various validation flags. If a flag is false, the corresponding error message is added to $this->messages and the function returns FALSE.

  • return: TRUE if all tests are passed, FALSE otherwise + messages in $this->messages
bool check_validation ()
clamscan_installed (line 3534)

try to locate clamdscan or clamscan on the server

This routine checks to see if either clamdscan or clamscan can be found somewhere. This is done via educated guessing. On success we return the path to the binary program file in $clamscan_path and the output of the version command in $clamscan_version. The funtion returns TRUE if we did find the clamscan program.

Note that we scan the directories with opendir/readdir/closedir rather than rely on file_exists() etc. because we would not find binaries with permissions 0711, which would otherwise be perfectly acceptable to execute with exec().

Also note that we suppress PHP-error messages that might be visible when we tread outside the open_basedir.

void clamscan_installed (string &$clamscan_path, string &$clamscan_version)
  • string &$clamscan_path: path to binary program (output)
  • string &$clamscan_version: version of the program we found (output)
construct_config_php (line 2330)

prepare a configuration file based on the collected information

  • return: string with contents of config.php
string construct_config_php ()
create_tables (line 3393)

create tables in database via include()'ing a file with tabledefs

  • return: TRUE on success, FALSE otherwise + messages written to $this->messages
  • uses: $DB
bool create_tables (string $filename)
  • string $filename: contains the table definitions
diff_microtime (line 3913)

Calculate the difference between two microtimes (borrowed from init.php)

  • return: interval between the two times (in seconds)
double diff_microtime (string $time_start, string $time_stop)
  • string $time_start: starting time as a string (fractional seconds, space, seconds)
  • string $time_stop: ending time as a string (fractional seconds, space, seconds)
end_session_and_redirect (line 2160)

unset installation data, end session and redirect the user to elsewhere

this redirects the user to one of the possible destinations as selected in the finish dialog (see show_dialog_finish()). Also, the INSTALL-array is unset, effectively erasing the collected data from the session. Subsequently the session cookie is reset, effectively ending the session. The effect is that the user has to start over if she returns to install.php. (The equivalent of 'logging off').

  • return: redirect + HTML-code sent to browser
void end_session_and_redirect ()
errorcount_bump (line 3096)

increment the error counter and perhaps slow things down

this routine counts the number of errors. It is used to count the number of attempts to guess a valid host/username/passord triplet. If the number of errors reaches 3, a delay is added (the installation program sleeps for a while). On every additional error an extra delay is added (3 seconds at a time) until the total delay reaches 24 seconds. At that point the collected data are reset and effectively the user has to start over.

  • return: error count incremented, optional delay introduced, and maybe data reset
void errorcount_bump ()
errorcount_reset (line 3113)

reset the error counter

this routine resets the effect of errorcount_bump().

  • return: error counter reset to 0 (implying no more delays)
void errorcount_reset ()
fetch_license (line 2879)

helper to retrieve the text of the LICENSE AGREEMENT for Website

  • return: a prime on error, 0 on success
int fetch_license (string &$license)
  • string &$license: receives ready-to-use HTML-code with the text of the license from /program/license.html
gd_supported (line 3641)

retrieve information about GD and supported graphics file formats

this routine determines whether GD is enabled and which graphics file formats are supported (check for GIF, JPG and PNG). If GD is not installed or if none of these three formats are supported, the routine returns FALSE. Details (version number, supported formats) are returned in &$details.

GIF is a special case due to patent issues: there is a distinction between read support and write support (see the PHP-documentation for details). GD-version >= 2.0.28 provides full support. This routine makes the distinction between fully supported (GIF: Yes), reading but not writing (GIF: Readonly) and not supported (GIF: No).

There is an issue with the return value of gd_info(). Here are two real world examples:

PHP 4.3.6 (Linux): gdinfo = Array ( [GD Version] => bundled (2.0.22 compatible) [FreeType Support] => 1 [FreeType Linkage] => with freetype [T1Lib Support] => [GIF Read Support] => 1 [GIF Create Support] => [JPG Support] => 1 [PNG Support] => 1 [WBMP Support] => 1 [XBM Support] => 1 [JIS-mapped Japanese Font Support] => )

PHP 5.3.5 (XAMPP): gdinfo = Array ( [GD Version] => bundled (2.0.34 compatible) [FreeType Support] => 1 [FreeType Linkage] => with freetype [T1Lib Support] => [GIF Read Support] => 1 [GIF Create Support] => 1 [JPEG Support] => 1 [PNG Support] => 1 [WBMP Support] => 1 [XPM Support] => [XBM Support] => 1 [JIS-mapped Japanese Font Support] => )

Note the subtle difference between 'JPG Support' and 'JPEG Support'. Checking for the first one of those yields a PHP Notice: "Undefined index: JPG Support (...)" in the second installation. *sigh* We work around it with isset(), on _all_ elements of gdinfo() to be certain.

  • return: TRUE if at least one file format is fully supported, FALSE otherwise
bool gd_supported (string &$details)
  • string &$details: returns detailed information about GD version and supported file formats
get_default_install_values (line 2200)

return an array with default configuration values

this routine tries to calculate/guess the best default values for config.php. We do so by looking in the global $_SERVER variable. If that doesn't work, we simply make up sensible values. In the end it is up to the user to enter the correct data; the values here are mere defaults.

A first-time install should be possible without changing/editing the default values, i.e. a standard Next-Next-Finish-type of installation.

  • return: array filled with sensible default values
  • todo: should we check the program version versus the stored program version here?
  • todo: there is something wrong with the default for $cms_www; FIXME (commented out for now)
array get_default_install_values ()
get_dialogdef_cms (line 1018)

fill an array with necessary information for the cms dialog

Note that this is a very light-weight implentation of the dialogdef idea used in the main program: we don't do fancy stuff with labels, hotkeys, etc. KISS, because I don't want to rely on all the libraries of the main program with all their interconnections and dependencies; the installer should more or less be a stand-alone application.

Note: the order of 'cms_demodata' and 'cms_demodata_password' is important: the field 'cms_demodata' must come first. If not, the validation of the password is not skipped if demodata is left unchecked by the user. See also save_cms().

  • return: array filled with field definitions and prompts etc.
array get_dialogdef_cms ()
get_dialogdef_database (line 728)

fill an array with necessary information for the database dialog

Note that this is a very light-weight implentation of the dialogdef idea used in the main program: we don't do fancy stuff with labels, hotkeys, etc. KISS, because I don't want to rely on all the libraries of the main program with all their interconnections and dependencies; the installer should more or less be a stand-alone application.

  • return: array filled with field definitions and prompts etc.
array get_dialogdef_database ()
get_dialogdef_finish (line 2094)

fill an array with necessary information for finish / jump dialog

Note that this is a very light-weight implentation of the dialogdef idea used in the main program: we don't do fancy stuff with labels, hotkeys, etc. KISS, because I don't want to rely on all the libraries of the main program with all their interconnections and dependencies; the installer should more or less be a stand-alone application.

  • return: array filled with field definitions and prompts etc.
array get_dialogdef_finish ()
get_dialogdef_installtype (line 417)

fill an array with necessary information for installtype dialog

Note that this is a very light-weight implentation of the dialogdef idea used in the main program: we don't do fancy stuff with labels, hotkeys, etc. KISS, because I don't want to rely on all the libraries of the main program with all their interconnections and dependencies; the installer should more or less be a stand-alone application.

  • return: array filled with field definitions and prompts etc.
array get_dialogdef_installtype ()
get_dialogdef_language (line 328)

fill an array with necessary information for language dialog

Note that this is a very light-weight implentation of the dialogdef idea used in the main program: we don't do fancy stuff with labels, hotkeys, etc. KISS, because I don't want to rely on all the libraries of the main program with all their interconnections and dependencies; the installer should more or less be a stand-alone application.

  • return: array filled with field definitions and prompts etc.
array get_dialogdef_language ()
get_dialogdef_user (line 1212)

fill an array with necessary information for the first user dialog

Note that this is a very light-weight implentation of the dialogdef idea used in the main program: we don't do fancy stuff with labels, hotkeys, etc. KISS, because I don't want to rely on all the libraries of the main program with all their interconnections and dependencies; the installer should more or less be a stand-alone application.

  • return: array filled with field definitions and prompts etc.
array get_dialogdef_user ()
get_list_of_install_languages (line 2845)

retrieve a list of available languages by querying the file system for install.php translation files

this routine constructs a list of language codes and language names (in the languages themselves) based on the language subdirectories available under /program/install/. The resulting array of code-name-pairs is sorted by name.

Note that because the names of the languages are expressed in the languages themselves, this routine has the side-effect of reading _all_ of the available language files into memory (see t()).

Also note that the sort order is based on UTF-8 byte sequences, i.e. the order is 7-bit ASCII bytes, followed by 2-byte, 3-byte and 4-byte UTF-8 sequences. No problem for this list of languages IMHO.

  • return: sorted list of available languages, keyed by language code
array get_list_of_install_languages ()
get_manifests (line 3366)

retrieve an array of manifests for modules, themes or languages

this examines the file system starting in the directory $path, looking for manifest files. These manifest files are named after the subdirectory they are in as follows. Example: If $path is /program/modules, this routine steps through that directory and may find subdirectories 'htmlpage', 'guestbook' and 'forum'. Eventually these manfest files are include()'d: /program/modules/htmlpage/htmlpage_manifest.php, /program/modules/guestbook/guestbook_manifest.php and /program/modules/forum/forum_manifest.php.

Every manifest file must describe the module (or language or theme) via the following construct:

    $manifests['htmlpage'] = array('name' => 'htmlpage', ...., 'cron_interval' => 0);

After processing all the subdirectories of $path, the resulting array $manifests is returned. Note that pseudo-directories like '.' and '..' are not considered. Also, subdirectories 'foo' without the file 'foo_manifest.php' are also ignored.

Note that the name of the manifest file itself is also stored in the array, but excluding the subdirectory name.

  • return: zero or more arrays comprising manifests
array get_manifests (string $path)
  • string $path: top directory for the search for manifest files
get_menu (line 2602)

construct a clickable menu which helps the user to jump back and forth in the funnel

this constructs a menu that allows the user to jump to a another step in the procedure when appropriate. Two parameters are important: the $dialog and the the $stage. The $dialog indicates the current dialog, i.e. the dialog that is currently displayed in the content area. This item in the menu is emphasised (e.g. the link is underlined via the style sheet). The $stage indicates how far we are in the procedure. The installation consists of some eight steps, and the used is encouraged to perform all steps in the natural order, by repeatedly pressing the [Next] button in the dialogs. Every time a dialog appears to have valid data, the $stage is incremented.

All the menu items after the current stage are greyed out and basically inaccessible for the user. Menu items before the current stage are accessible, so it is possible to jump backwards to dialogs that were already processed but the only way to advance to a new screen is to use the [Next] (and provide valid data, obviously).

The greyed-out menu items have a href property consisting of a simple "#". This is interpreted by the browser as a relative link within the current page. The effect is that the current page stays on the screen, including any unsaved data the user may already have entered. By showing the links in a different colour (grey and not blue), the user can visually see which items are clickable and which are not.

By showing all the menu items and greying-out the inaccessible ones we effectively build a funnel and at the same time indicating which steps will follow in the procedure.

There is a special case when $stage hits the FINISH-dialog. If the user has not yet reached that stage, all dialogs before the finish-dialog are active and the rest is greyed-out. Once the $stage reaches the FINISH-dialog, all precious dialogs become instantly unreachable. This is because the step between the confirmation dialog and the finish dialog is the place where the actual installation takes place. That is a one-time operation and the user should not be able to jump backwards and change data after all the actual installation work is already done.

Note that the menu item to download the config.php is displayed before the finish dialog. This is because it appears more logical to me (but YMMV).

  • return: ready-to-use HTML-code
string get_menu (int $dialog, int $stage, [string $m = ''])
  • int $dialog: indicates the current dialog
  • int $stage: indicates the highest numbered dialog that was already reached
  • string $m: margin for better readability of generated HTML-code
get_options_db_type (line 3062)

construct a list of database options

This constructs an array with key-value-pairs indicating all available databases. Currently (february 2015) there is only one database supported: MySQL. However, for backwards compatibility both the 'mysqli' and the deprecated 'mysql' APIs are supported. In the future we may support more databases, such as PostgreSQL.

Note that we present ordinary users with a single choice (MySQL) while distinguishing between the two variants. Anyone using the debug-option will have an exact choice between the two flavours.

  • return: list of available database choices (could be empty)
array get_options_db_type ()
get_page (line 2391)

construct a complete HTML-page that can be sent to the user's browser

this routine constructs a string containing the complete page to send to the user's browser, starting with the <html> opening tag and ending with the </html> closing tag. The constructed page is returned as a string.

This routine also peeks into the INSTALL-array, e.g. for the language key and the high_visibility flag.

If $this->messages is not empty, the items in that array are displayed between the page header (logo+helpbutton) and the menu/content area. This is the feedback of the previous action to the user (if any).

Note that we hard-code the content type to be "text/html; charset=UTF-8" in a http-equiv right at the top of the &lt;head&gt;. This indicates the correct charset to the browser even if a generated page is stored by the user and later retrieved in which case there will be no http-header. (We cannot be sure that the user's browser correctly identifies the stored document as UTF-8 if that is not the default).

  • return: ready-to-use HTML-code that can be sent to the user's browser
  • todo: should we promote language and high_visibility to function parameters instead of using $_SESSION directly?
string get_page ([string $dialog_title = ''], [string $menu = ''], [string $content = ''], [string $help_topic = 'install'])
  • string $dialog_title: text to show in the browser's title bar (indicating where we are)
  • string $menu: ready-to-use HTML-code comprising the menu at the left hand side of the page
  • string $content: ready-to-user HTML-code holding the actual contents of the page
  • string $help_topic: the topic or subtopic in the manual to link to
get_utf8_parameter_string (line 2950)

return a valid (unquoted) UTF-8 string parameter typically from $_POST, or default value if none

this retrieves a parameter string, typically from $_POST, and checks basic properties.

  1. the string should not contain more than $maximum_length characters
  2. the string should be valid UTF-8
If it is not a valid UTF-8 string or the string is too long, $error_value (default: empty string) is returned. If no string by the name $name is present in $gpc, the default value $default_value is returned. If there WAS a valid UTF-8 string, it is automagically UNquoted in the process.

Note that we check the stringlength in two steps. First we check the worst-case UTF-8 string with 4-byte sequences for a length of 4 * $maximum_length. Then, after establishing that the string is valid UTF-8 we make a more precise check of the length expressed in chars rather than bytes.

  • return: the value of the parameter or the default value if not specified
mixed get_utf8_parameter_string (array &$gpc, string $name, [mixed $default_value = ''], [mixed $error_value = ''], [mixed $maximum_length = 255])
  • array &$gpc: a reference to the array to search (usually $_POST, $_GET or $_COOKIE)
  • string $name: the name of the variable to retrieve
  • mixed $default_value: the value to return if parameter is not in $gpc
  • mixed $error_value: the value to return if parameter is not valid UTF-8
  • mixed $maximum_length
guess_url (line 2694)

educated guesses for scheme, host and portname from $_SERVER

this routine tries to guess the various components of the url that was used to reach this script, based on the information in the global array $_SERVER. If no information can be guessed at all, the result is something like 'http://localhost'.

Note that the 'authority' is a combination of hostname and portnumber, but only if the portnumber is non-standard. For http and port 80, and https and port 443 the portnumber is suppressed, because these are the default ports for those schemes.

Note that we actually guess the url that should correspond with the document root.

  • return: array with various components of guessed url
array guess_url ()
insert_tabledata (line 3425)

fill tables in database via include()'ing a file with tabledata

  • return: TRUE on success, FALSE otherwise + messages written to $this->messages
  • uses: $DB
bool insert_tabledata (string $filename)
  • string $filename: contains the table definitions
invisible_test_image (line 3900)

create a link to an invisible image to test the friendly URL feature

this routine is part of a trick to discover whether the web server understands a frienly url like /index.php/1/23/Welcome rather than /index.php?area=1&node=23. This trick works as follows.

  1. By default we assume that the webserver does NOT understand friendly url's by setting the corresponding install parameter to FALSE (see get_default_install_values()
  2. In the 5th step (after the Database dialog, ie. 'hidden' behind a password) we embed an invisible image (see below) in the dialog.
  3. The browser will attempt to retrieve the image and to (not) show it. This means that we call $WAS_SCRIPT_NAME with a friendly url appears to point to 1x1.gif.
4a.IF (and only if) the webserver understands this friendly URL, we arrive in run() with _SERVER['PATH_INFO'] set to /friendly/test/1x1.gif. This gives us a chance to flip the parameter 'friendly_url' in the installation parameters from the default FALSE to TRUE 4b.If the webserver does NOT understand the friendly url, the image file will never be found, and we will never return the 1x1.gif and also never flip the setting from FALSE to TRUE. 5. Done

I chose the Website dialog for this trick, because you can only get there after you have entered a valid username/password for the database. This means that only genuine users will perform this trick; outsiders never get to that dialog without a valid password.

Obviously the setting is eventually entered in the configuration table, just like the title and the website_from_address etc.

Note: both the Install Wizard itself and the code that generates the test image (in run()) access the session variables. According to the PHP documentation this works because the file that holds the session information is locked between the calls to session_start() and session_write_close(). The only visible effect _might_ be that the one call has to wait for the other to finish. Oh well, it is only a 1x1 gif of 35 bytes. It probably will go unnoticed.

string invisible_test_image ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
is_already_installed (line 3271)

check for previous install

this routine checks to see if another installation should be allowed. Returns TRUE if the program was already installed or FALSE otherwise.

void is_already_installed ()
magic_unquote (line 2916)

this circumvents the 'magic' in magic_quotes_gpc() by conditionally stripping slashes

This routine borrowed from waslib.php.

  • return: the unescaped string
string magic_unquote (string $value)
  • string $value: a string value that is conditionally unescaped
microtime (line 3928)

a trick for systems without the microtime function

if the system function does not exist, we return the current time with a 1-second resolution.

  • return: microseconds always 0.0 and current timestamp (seconds)
string microtime ()
mysql_close (line 3855)

unified mysql/mysqli wrapper for close

  • return: FALSE on error
int|bool mysql_close (string $db_type, string $db_link)
  • string $db_type: is either mysql or mysqli
  • string $db_link
mysql_connect (line 3735)

unified mysql/mysqli wrapper for connect

  • return: FALSE on error
resource|object|bool mysql_connect (string $db_type, string $server, string $username, string $password)
  • string $db_type: is either mysql or mysqli
  • string $server: with optional port number eg. "dbserver:3306"
  • string $username
  • string $password
mysql_get_server_info (line 3755)

unified mysql/mysqli wrapper for get_server_info

  • return: FALSE on error, otherwise version string
string|bool mysql_get_server_info (string $db_type, string $db_link)
  • string $db_type: is either mysql or mysqli
  • string $db_link
mysql_num_rows (line 3839)

unified mysql/mysqli wrapper for num_rows

  • return: FALSE on error, # of rows in result otherwise
int|bool mysql_num_rows (string $db_type, string $result)
  • string $db_type: is either mysql or mysqli
  • string $result
mysql_query (line 3772)

unified mysql/mysqli wrapper for query

  • return: FALSE on error
resource|object|bool mysql_query (string $db_type, string $db_link, string $query)
  • string $db_type: is either mysql or mysqli
  • string $db_link
  • string $query
mysql_real_escape_string (line 3806)

unified mysql/mysqli wrapper for real_escape_string

  • return: FALSE on error, escaped string otherwise
string|bool mysql_real_escape_string (string $db_type, string $db_link, string $str)
  • string $db_type: is either mysql or mysqli
  • string $db_link
  • string $str: unescaped string
mysql_select_db (line 3789)

unified mysql/mysqli wrapper for select_db

  • return: FALSE on error
string|bool mysql_select_db (string $db_type, string $db_link, string $db_name)
  • string $db_type: is either mysql or mysqli
  • string $db_link
  • string $db_name
mysql_set_charset (line 3823)

unified mysql/mysqli wrapper for set_charset

  • return: FALSE on error, escaped string otherwise
string|bool mysql_set_charset (string $db_type, string $db_link, string $charset)
  • string $db_type: is either mysql or mysqli
  • string $db_link
  • string $charset
mysql_utf8mb3 (line 3718)

massage string to contain only 3-byte UTF8-sequences

this replaces an otherwise perfectly valid 4-byte UTF-8 sequence in $utf8str with a 3-byte UTF-8 sequence equivalent with the Unicode replacement character U+FFFD.

The effect is that it leaves a hint that there used to be some character instead of silently discarding 4-byte sequences which MySQL does.

  • return: UTF-8 string with all 4-byte sequences replaced
string mysql_utf8mb3 (string $utf8str)
  • string $utf8str: valid UTF-8 encoded string
mysql_utf8_support (line 3688)

determine the level of UTF-8 support based on MySQL-server version

MySQL support for UTF-8 was non-existent before 4.1.x and limited until 5.5.3. In this context 'limited' means: only the Basic Multilingual Plane (U+0000 ... U+FFFF) is supported, i.e. a maximum of 3-byte sequences per character.

As of 5.5.3 the full UTF-8 specification according to RFC 3629 is implemented. MySQL now has 'invented' yet another proprietary name for this character set: 'utf8mb4' (WTF?), and introduces the alias 'utf8mb3' for the pre 5.5.3 limited support for 'utf8' (WTF??), hinting that the meaning of 'utf8' may change in future versions to indicate 'utf8mb4' (WTF???). IM(NS)HO this is yet another reason to go looking for a decent replacement for MySQL. YMMV.

This routine returns exactly one of the values below (based on the server version).

  • 0: there is no UTF-8 support available in this server
  • 3: the limited 3-byte sequences are supported
  • 4: full support for 4-byte sequences available, but using the stupid ad-hoc name 'utf8mb4'
or the value FALSE if version information could not be obtained.

  • return: support level: either 0 (none), 3 (limited), 4 (full, but with a quirky name) or FALSE on error
int|bool mysql_utf8_support (string $db_type, resource $db_link)
  • string $db_type: is either mysql or mysqli
  • resource $db_link: the MySQL connection
perform_installation (line 1628)

perform the actual initialisation of the cms

this routine initialises the database: creates tables, inserts essential data (first user account, other defaults) and optional demonstration data.

The strategy is as follows.

  • (1) manufacture a database object in the global $DB
  • (2A) create the main tables (from /program/install/tabledefs.php)
  • (2B) insert essential data (from /program/install/tabledata.php)
  • (2C) store the collected data (website title, etc.),
  • (3) if necessary, create the data directory
  • (4) record the currently available languages in the database
  • (5) create the first useraccount,
Once the main part is done, install modules and themes based on the relevant information that is stored in the corresponding manifest-file by performing the following steps for each module and theme:

  • (6A) insert a record in the appropriate table with active = FALSE
  • (6B) create the tables (if any tables are necessary according to the manifest)
  • (6C) install the item by including a file and executing function <item>_install()
  • (6D) flip the active flag in the record from step 5A to indicate success
Subsequently the optional demodata is installed.

  • (7A) a foundation is created via the function demodata() from /program/install/demodata.php
  • (7B) all modules + themes can add to the demo data via the appropriate subroutines
If all goes well, this routine ends with an attempt to

  • (8) save the config.php file at the correct location. (it is not an error if that does not work; it only means that the user has to upload the config.php file manually.

  • return: TRUE on success, FALSE otherwise + messages in $this->messages[]
  • todo: should we save the config.php to the datadir if the main dir fails? Mmmm.... security implications?
  • todo: this routine badly needs refactoring
bool perform_installation ()
quasi_random_string (line 3463)

generate a string with quasi-random characters

This routine borrowed from waslib.php.

Note that this too is an ASCII-centric routine: we only use plain ASCII letters and digits and nothing of the 64000 other UNicode characters in the Basic Multilingual Plane. The reason is simple: 7-bit ASCII characters have the best chance of getting through communication channels unmangled so there.

void quasi_random_string (int $length, [int $candidates = 36])
  • int $length: length of the string to generate
  • int $candidates: number of candidate-characters to choose from
render_dialog (line 2985)

quick and dirty dialogdef renderer

This is a small routine to render simple dialogs with strings, lists and checkboxes.

Note that every element in the $dialogdef is in itself an array. Recognised elements in those arrays are:

  • label (string): displayed before the actual input element
  • help (string): additional information for the user, rendered after/under the input element
  • value (mixed): the current value of the input element
  • show (bool): if TRUE, the element is displayed/rendered, otherwise it is simply skipped
  • type (enum): 's'=>string (text), 'p'=>password, 'l'=>list, 'b'=>bool(checkbox)
  • options(array): array with key-value-pairs with acceptable choices (used in 'l' (list) elements)
  • minlenght(int): minimum length of an input string or password
  • maxlenght(int): maximum length of an input string or password
The names of the input elements are copied from the keys used in $dialogdef.

  • return: ready-to-use HTML-code comprising the rendered dialog
string render_dialog (array $dialogdef, string $m)
  • array $dialogdef: contains labels, values and other information describing input elements
  • string $m: improves readablity of generated code
run (line 161)

main dispatcher for the Installation Wizard

This routine termines what needs to be done and does it by calling the corresponding workhorse routines.

Note the special trick with the test for a friendly URL; see invisible_test_image() for the gory details.

void run ()
sanitise_filename (line 3487)

sanitise a string to make it acceptable as a filename/directoryname

This routine more or less borrowed from waslib.php.

Note that this routine too is very ASCII-centric: in the end only ASCII-characters (52 letters, 10 digits and dash, dot and underscore) are allowed in the resulting file/directory name. However, by first mapping UTF-8 to ASCII (getting rid of diacriticals) we can make names more readable.

  • return: sanitised filename which is never empty
string sanitise_filename (string $filename)
  • string $filename: the string to sanitise
save_cms (line 852)

validate and store the CMS-data the user supplied

This is the companion routine for show_dialog_cms(). It stores the user-supplied data about the website (paths etc.) We always store the data in the global _SESSION, even if something goes wrong. This makes that the user will use the latest values when re-doing the dialog.

We try to validate the specified directories: they should at least exist. The datadirectory should also be writable by us. If it not exists we try to create it (and remove it after the test). Note that the PHP safe_mode may complicate things here.

  • return: TRUE on success, otherwise FALSE + error information in $this->messages array
  • todo: also take safe_mode into account? Should that be a requirement for succesfull installation?
bool save_cms ()
save_database (line 594)

validate database information

This is the companion routine for show_dialog_database(). It stores the user-supplied data about the database. We always store the data in the global _SESSION, even if something goes wrong. This makes that the user will use the latest values when re-doing the dialog. However, only when the values are valid, the parameter db_validated is set to TRUE. This is used lateron (in the confirmation phase).

This routine doubles as a gate keeper. Every time the user makes a mistake, an error counter is incremented and the script pauses for some time (see errorcount_bump()). If there are too many errors, the script resets the data collected sofar and the procedure starts from scratch. This is a (probably futile) attempt to make it harder to brute force an entry by repeatedly probing for database credentials. Unfortunately there is no easy way (at least one I can think of) to protect this script from repeated break-in attempts other than by simply removing or renaming this script. Oh well.

The following tests are performed:

  • fields should not fail basic tests (min/max stringlength etc.)
  • the database is not supposed to be in the list of 'forbidden' databases (e.g. 'test' or 'mysql')
  • prefix must only use ASCII-letters A-Z or a-z, digits 0-9 or an underscore
  • prefix must start with an ASCII-letter
If the above conditions are not satisfied we bail out immediately, without testing other information. Otherwise, we also perform these tests:
  • is it possible to connect to the database server
  • is it possible to select the specified database
  • are there no tables in the database that start with the prefix
If something needs to be done about the prefix and we are in standard mode, we automatically switch to custom mode, allowing the user to edit the prefix too.

  • return: TRUE on success, otherwise FALSE + error information in $this->messages array
bool save_database ()
save_installtype (line 384)

store the selected install type + high visibility flag

This is the companion routine for show_dialog_installtype(). It stores the user-supplied choices for custom install and high visibility. The values in the radio button should be 0 for standard and 1 for custom install. The value we keep is a boolean indicating a custom install yes or no.

  • return: TRUE on success
bool save_installtype ()
save_language (line 298)

store the selected language

This is the companion routine for show_dialog_language(). It validates and stores the user-supplied language key.

  • return: TRUE on success
bool save_language ()
save_user (line 1160)

validate and store the data for the first user account

This is the companion routine for show_dialog_user(). It stores the information about the first user account. We always store the data in the global _SESSION, even if something goes wrong. This makes that the user will use the latest values when re-doing the dialog.

We try to validate at least the password:

  • minimum of 8 characters
  • minimum 1 upper case, 1 lower case, 1 digit
Also, both username and full name should not be empty

  • return: TRUE on success, otherwise FALSE + error information in $this->messages array
bool save_user ()
show_dialog_cancelled (line 2126)

show the user that the process has been cancelled

this shows a screen with the message that the installation procedure has been cancelled. There is a single [OK] button that effectively allows the user to try again. The existing session (and all the data it might contain) is destroyed.

Note that we first construct the page (possibly in another language) in a separate variable before we destroy all data collected sofar.

  • return: HTML-code sent to browser
void show_dialog_cancelled ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
show_dialog_cms (line 811)

construct the dialog for essential cms data (title, paths, e-mail address)

This dialog contains the following fields:

  • website_title (varchar(255))
  • website_from_address (varchar(255))
  • website_replyto_address (varchar(255))
  • cms_dir (varchar(240))
  • cms_www (varchar(240))
  • cms_progdir (varchar(240))
  • cms_progwww (varchar(240))
  • cms_dataroot (varchar(240))
  • cms_demodata (boolean)
  • cms_demodata_password (varchar(255) (but only a sha1() or md5() hash is stored eventually)
Some fields are suppressed in the dialog if the user selected a Standard installation:
  • website_replyto_address: copied from website_from_address
  • cms_progdir: constructed from cms_dir
  • cms_progwww: constructed from cms_www
Note the special trick with the test for a friendly URL; see invisible_test_image() for the gory details.

void show_dialog_cms ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
show_dialog_compatibility (line 1265)

construct the comptibility overview

this routine displays a tabular overview of minimal compatibility requirements and the current status/testresults. The table is constructed in a subroutine; we only deal with the display here.

Q: Why here, at the last stop before installation? A: Because we otherwise would leak information about our environment to complete strangers (assuming anyone can execute this script).

  • return: HTML-code sent to browser
  • todo: more tests to perform here: safe mode, memory limit, processing time limit, register globals
void show_dialog_compatibility ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
show_dialog_confirm (line 1487)

construct the overview/confirmation dialog

This dialog contains an overview of the information entered (excluding the passwords which are visually replaced with asterisks). This is the last chance the user gets to change the data entered. Once the user presses the [Next] button, the actual installation takes off.

  • return: HTML-code sent to browser
void show_dialog_confirm ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
show_dialog_database (line 536)

construct the dialog for database (server, host, username, password, etc.)

This dialog contains the following fields:

  • db_type (pick from enumerated list)
  • db_server (varchar(240))
  • db_username (varchar(240))
  • db_password (varchar(240))
  • db_name (varchar(240))
  • db_prefix (varchar(240))
One field is suppressed in the dialog if the user selected a Standard installation:
  • db_prefix

  • return: HTML-code sent to browser
void show_dialog_database ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
show_dialog_finish (line 2040)

construct the finish screen

this dialog is displayed after a succesful installation. The user is prompted to select the next destination, which can be either admin.php (the backoffice), index.php (the frontpage), manual.php (the documentation) or the project's home page.

There is also an option to download config.php. Once the user's choice is submitted, the session is reset and config.php can no longer be downloaded. This session reset is done in the 'Done' dialog (see end_session_and_redirect()).

Note that the boolean parameter INSTALL['config_php_written'] indicates whether the file config.php was already succesfully written in the designated location. If this is the case, we show a different message compared to the case where the user still needs to download+upload config.php.

  • return: HTML-code sent to browser
void show_dialog_finish ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
show_dialog_installtype (line 352)

construct the installtype + high visibility selection dialog

This dialog contains a radio button where the user selects 'standard' or 'custom' and also a checkbox for high visibility. As always the dialog ends with buttons to move forward, backward or to cancel the installation process alltogether.

  • return: HTML-code sent to browser
void show_dialog_installtype ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
show_dialog_language (line 269)

construct the language selection dialog

this dialog allows the user to pick a language. The choices are determined by looking for translation files in the file system, specifically for files /program/install/languages/LL/install.php where LL is the language code, see get_list_of_install_languages().

  • return: HTML-code sent to browser
void show_dialog_language ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
show_dialog_license (line 462)

construct a full license agreement and an input where the user must enter 'I agree'

This constructs a (long) license agreement dialog. We more or less force the user to actually scroll through it by having the input box + buttons after the agreement text. Note that the phrase to enter into the box is also translated, it may be 'I agree' in English but something else in other languages (done via translation file). If the user already accepted the license, the 'I agree'-text is already displayed in the dialog, in the current language. (A user could type 'I agree' in English, change the language into Dutch, in which case the $value in the textbox would be something like 'Ik ga accoord' or whatever the translation of 'I agree' is.) This is done via an extra level of translation with the 'dialog_license_i_agree' translation in the prompt.

Also, the instruction to enter the exact words are repeated near the bottom of the screen, just to make sure the user understands what to do.

Note that the phrase the user enters is compared to the requested phrase in a more or less case-INsensitive way (see check_license().

  • return: HTML-code sent to browser
void show_dialog_license ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
show_dialog_user (line 1119)

construct the dialog for the first user account

This dialog contains the following fields:

  • user_username (varchar(255))
  • user_full_name (varchar(255))
  • user_email (varchar(255))
  • user_password (varchar(255) (but only a sha1() or md5() hash is stored eventually)
An additional feature of this routine is to set a default email address for the account, by copying the address that can be found in the Reply-To: field that was entered earlier (or constructed from the From: address).

  • return: HTML-code sent to browser
void show_dialog_user ([string $m = ''])
  • string $m: margin for better readability of generated HTML-code
t (line 2774)

retrieve a translated string with optional parameters filled in

this routine tries to find a translated string based on the $key. If $replace is not empty, any keys in that array that are found in the translation are replace by the corresponding value.

The translations are read from the files /program/install/languages/LL/install.php, where LL is a language code (usually two-letter e.g. en, nl, de, fr). If the desired language is not available, English is used instead. If the requested translation is not found, the key of the translation is returned, sandwiched between the language codes. Usually this does not happen (all keys used have a translation), but it can be helpful when creating and testing a new translation.

Note that a language file is retrieved completely. This means that all the translations are read from file in one swoop; there is no need to go to the disk for every individual translation. The translation file is buffered in memory via the static variable $phrases, which is an array keyed by language. That means that multiple languages can co-exist in this static variable. That last feature is used in constructing a list of available languages where the name of the language is expressed in the language itself (see get_list_of_install_languages()).

Because the English language is the basis for all other languages, we always read the English translation the first time around, as a backup plan for missing prompts in other languages.

Note: By convention the keys in $replace are upper case words, with optional underscores, sandwiched between curly braces. Examples: '{FIELD}', '{DATABASE}', '{RELEASE_DATE}'. The idea is that these words make life easier for translators.

  • return: translated text with incorporated parameters from $replace
string t (string $key, [array $replace = ''], [string $lang = ''])
  • string $key: the key in the string-array with translations
  • array $replace: key-value pairs used for search/replace in the translated string
  • string $lang: indicates which language we should translate into
validate (line 3130)

minimal validation of data input

this routine is a KISS-validator; we check for min/max stringlengths in strings and passwords (defaults: 0 and 255) and a valid item in listboxes. In case of error, a message is added to $this->messages and the function returns FALSE. On success we return TRUE. Note that additional validation could or should be done on some fields, e.g. a password of sufficient complexity, etc. This routine does not do that.

  • return: if minimal tests passed, otherwise FALSE + messages added to $this->messages
TRUE validate (array $item, string $value)
  • array $item: this array holds a field definition from a dialog definition
  • string $value: this is the magic_unquote()'d and trim()'ed UTF-8 compliant string value the user POSTed
validate_password (line 3206)

validation of password input

this routine analyses the password provided against the minimal requirements of password complexity. Here 'complexity' means a minimum number of upper case and lower case characters and a minimum number of digits. The required 'complexity' of a password is quite ASCII-centric. The idea is to make a user pick from a pool of at least 62 different characters rather than say 26 lowercase latin letters a-z. However, with UTF-8 the story is actually different: what is more "complex": "Aa1" or U+1F150 NEGATIVE CIRCLED LATIN CAPITAL LETTER A encoded as "\xF0\x9F\x85\x90"? Even though it is only 1 character, the result is byte string of length 4 rather than the meager 3-byte string that satisfies the minimum requirements...

Anyway. In order to take into account some additional upper case and lower case characters without resorting to loading the complete Unicode database, I opt for the following tricks. If lowercase($str) differs from $str itself, it was apparently not a lower case character but probably an upper case (or title case) character; good enough to count it toward the upper case character count. The same idea is used for the opposite: if upper($str) != $str => $str probably lowercase.

This algoritm works for A-Z and a-z but also for the 1100+ other Unicode characters that are case-aware. So, the user isn't held back if she insists on the password p a \xE1 \xBA \x9E w 0 r d (where \xE1\xBA\x9E equates to U+1E9E LATIN CAPITAL LETTER SHARP S

Note that by default we still require at least 1 digit 0-9 and do not count any of the 410 "digits" like these zeros: U+0660: ARABIC-INDIC DIGIT ZERO U+06F0: EXTENDED ARABIC-INDIC DIGIT ZERO U+07C0: NKO DIGIT ZERO U+0966: DEVANAGARI DIGIT ZERO ... U+1D7EC: MATHEMATICAL SANS-SERIF BOLD DIGIT ZERO U+1D7F6: MATHEMATICAL MONOSPACE DIGIT ZERO

Perhaps some other time... (Did I mention this is quite ASCII-centric?)

  • return: if tests passed, otherwise FALSE + messages added to $this->messages
TRUE validate_password (string $label, string $password, [int $min_lower = 1], [int $min_upper = 1], [int $min_digit = 1])
  • string $label: this string holds the human-readable field name
  • string $password: this is the magic_unquote()'d and trim()'ed password the user POSTed
  • int $min_lower: the minimum number of lower case letters
  • int $min_upper: the minimum number of upper case letters
  • int $min_digit: the minimum number of digits
write_config_php (line 3308)

attempt to write the file config.php in the correct location

this routine tries to write the file config.php. If this fails, we return FALSE, otherwise TRUE. Note that the permissions of the file are set to read-only (chmod 0400) for the owner of the file, and nothing for group and world. That should be enough for the webserver.

  • return: TRUE on success, FALSE otherwise
  • todo: should we make the filemode (hardcoded at 0400) configurable/customisable?
bool write_config_php ()

Documentation generated on Tue, 28 Jun 2016 19:09:54 +0200 by phpDocumentor 1.4.0