The Low Road for Integration with a WordPress Site
WordPress is everywhere. While the usual contract situation for this platform involves integrating a WordPress blog into an existing website, given the growing number of sites that run entirely on WordPress it is not uncommon to find oneself in the reverse situation. Given an existing web application with its own database and administrative backend, the challenge is then to integrate it into the client's primary WordPress site. I recently had cause to do just this for an antiquated custom newsletter/CRM application, old enough to be built around its own microframework rather than an existing PHP framework. If you're ever tempted to do this, don't; it tends to raise the cost of ownership down the line since (a) everyone who works with the application has more to learn, and (b) there is always the burning question of whether or not the author was good enough to avoid the creation of security holes. This particular application was written in PHP, so in principle it was possible to refactor it as a plugin and thus pull it entirely within WordPress. This, however, would have been a major undertaking, less a refactor and more a complete rewrite, far from plausible within the constraints of time and budget.
So how does one quickly hack together an integration for a PHP application, inserting it into a WordPress site? The first pass in this case actually turned out to be fairly simple, and required little in the way of code changes:
- Place the application code into a subfolder under the WordPress webroot.
- Add the application database to the same database server used by WordPress.
- Give the application access to the WordPress database as well as its own.
- Change the application administrative login code to authenticate against the WordPress user data.
- Set up a WordPress cron job that generates site header and footer HTML files on a regular basis.
- Remove the existing header and footer from the application, and replace them with code to load the header and footer HTML generated by WordPress.
As things stand this approach should not require all that much more work even for applications written in a language other than PHP or that do use an established framework, or that would require multiple servers with a proxy in front to appear as a seamless single site. The basic principle is the same regardless: integrate the administrative login, and find a way to have WordPress export the site header and footer as HTML for the application to use. This is good enough as a first pass for many types of integration, and other linkages can be added incrementally as needed. That it is fast and cheap is a compelling reason to try it this way, provided it fits the requirements.
Login Integration
Sorting out login integration is straightforward if the application is written in PHP, even if using the core MySQL functionality directly rather than issuing queries via a framework. The password hashing code used by WordPress is portable and can just be copied over to where it needs to be from wp-includes/class-phpass.php
. If you want to allow WordPress administrators to log in to the integrated application, for example, the following works well enough:
/** * A simple parameterized query function, with safe token replacement. * * Usage: query("select x from y where b = '%d' and c = '%s'", $d, $s) */ function query($sql) { $args = func_get_args(); array_shift($args); query_preg_callback($args, true); $processed_sql = preg_replace_callback( '/(%d|%s|%%|%f|%b|%n)/', '_query_preg_callback', $sql ); $result = mysql_query($processed_sql, get_connection()); $error = mysql_error(); if($error) { error_log($error . ' SQL: ' . $processed_sql); } return $result; } /** * Cheerfully stolen from Drupal. Don't try to write your own parameterized * SQL code; it won't go well. */ function query_preg_callback($match, $init = false) { static $args = NULL; if ($init) { $args = $match; return; } switch ($match[1]) { case '%d': // We must use type casting to int to convert FALSE/NULL/(TRUE?) $value = array_shift($args); // Do we need special bigint handling? if ($value > PHP_INT_MAX) { $precision = ini_get('precision'); @ini_set('precision', 16); $value = utf8_sprintf('%.0f', $value); @ini_set('precision', $precision); } else { $value = (int) $value; } // We don't need db_escape_string as numbers are db-safe. return $value; case '%s': return mysql_real_escape_string(array_shift($args), get_connection()); case '%n': // Numeric values have arbitrary precision, so can't be treated as float. // is_numeric() allows hex values (0xFF), but they are not valid. $value = mb_trim(array_shift($args)); return is_numeric($value) && !preg_match('/x/i', $value) ? $value : '0'; case '%%': return '%'; case '%f': return (float) array_shift($args); case '%b': // Binary data. return "'" . mysql_real_escape_string(array_shift($args), get_connection()) . "'"; } } /** * Return true if authenticated, false otherwise. */ function is_authenticated_via_wordpress($username, $password) { $sql = "select u.user_login as username, u.user_pass as password" . " from wp_users u inner join wp_usermeta m" . " on u.id = m.user_id" . " where u.user_login = '%s'" . " and m.meta_key = 'wp_capabilities'" . " and m.meta_value like '%%administrator%%'"; $result = query($sql, $username); if(!$result || !($row = mysql_fetch_assoc($result))) { return false; } // Replace the fake path here with whatever the real absolute // path to the file might happen to be. require_once '/absolute/path/to/wp-includes/class-phpass.php'; $wp_hasher = new PasswordHash(8, true); return $wp_hasher->CheckPassword($password, $row['password'])); }
If you do happen to be using another language, then the same strategy applies, but you must port the hashing code in class-phpass.php
rather than making use of it as is. Fortunately it isn't a lot of code, and the job isn't terribly hard, given that the results can be tested against existing records.
Exporting Header and Footer Files from WordPress
The code needed to export header and footer HTML files from WordPress can be added to a theme or plugin. For the purposes of this example, a plugin is assumed. The strategy is fairly simple when seen in hindsight, involving the use of temporary filters, adjusted settings, and a global flag to suppress or change things like page title. It does require some experimentation to find the necessary alterations, however. Beyond the examples given, it is probably the case that other alterations will be needed for any specific use case. Since the existing include files will be in use, care is taken to ensure that updating the file is atomic.
/** * Write out header and footer templates generated from the current files. */ function example_plugin_write_includes() { global $example_is_generating_includes, $wp_query; $header_file = $_SERVER['DOCUMENT_ROOT'] . '/generated/header.html'; $header_file_tmp = $header_file . '.tmp'; $footer_file = $_SERVER['DOCUMENT_ROOT'] . '/generated/footer.html'; $footer_file_tmp = $footer_file . '.tmp'; // Much hackery to get the header to do the right things. Make the code think // that this is a page, set a global flag that can be referred to in templates // where necessary, and force the title to be different. $example_is_generating_includes = true; add_filter( 'wp_title', 'example_plugin_includes_force_title' ); add_filter( 'document_title_parts', 'example_plugin_includes_force_title_parts' ); $prior_is_page = $wp_query->is_page; $wp_query->is_page = true; // Write the page header into a buffer and then a variable. ob_start(); get_header(); $content = ob_get_contents(); ob_end_clean(); // Atomic swap of file. Content should be UTF-8 throughout. file_put_contents($header_file_tmp, $content); rename($header_file_tmp, $header_file); // Write the page footer into a buffer and then a variable. ob_start(); get_footer(); $content = ob_get_contents(); ob_end_clean(); // Atomic swap of file. Content should be UTF-8 throughout. file_put_contents($footer_file_tmp, $content); rename($footer_file_tmp, $footer_file); // Undo all the changes needed to make the generation run. $example_is_generating_includes = false; remove_filter( 'wp_title', 'example_plugin_includes_force_title' ); remove_filter( 'document_title_parts', 'example_plugin_includes_force_title_parts' ); $wp_query->is_page = $prior_is_page; } /** * Used as a temporary filter when building includes. */ function example_plugin_includes_force_title() { return 'Alternative title for Integrated Application'; } /** * Used as a temporary filter when building includes. */ function example_plugin_includes_force_title_parts($parts) { return array( 'title' => example_plugin_includes_force_title() ); }
With this code in hand, the next step is to set it running every so often as a cron job. Here a 15 minute schedule is added, and the task of generating the include files added to that schedule. This assumes the code to reside in a plugin.
// Add the 15 minute cron schedule when the plugin is activated. function example_plugin_activation() { wp_schedule_event(time(), '15-minutes', 'example_plugin_15_minute_cron'); } register_activation_hook(__FILE__, 'example_plugin_activation'); // Remove the cron schedule when the plugin is deactivated. function example_plugin_deactivation() { wp_clear_scheduled_hook('example_plugin_15_minute_cron'); } register_deactivation_hook(__FILE__, 'example_plugin_deactivation'); // Since WordPress doesn't have a 15 minute cron schedule by default, add one. function example_plugin_add_new_cron_schedules($schedules) { $schedules['15-minutes'] = array( 'interval' => 15 * 60, 'display' => __('Every 15 minutes') ); return $schedules; } add_filter('cron_schedules', 'example_plugin_add_new_cron_schedules'); add_action('example_plugin_15_minute_cron', 'example_plugin_write_includes');
Lastly, add an entry to the server crontab to ensure that the WordPress cron jobs are triggered at an appropriate frequency. Something along these lines will suffice:
*/15 * * * * curl --insecure -sS https://localhost/wp-cron.php >/dev/null 2>&1
Now these include HTML files will be ready and in place for the integrated application to load and wrap around its pages.