WordPress Stuff

Useful Wordpress functions.php functions.

A Convenient Debugging Facility

This adds a DEBUG() function that collects debug strings. The code can be pasted into the theme’s functions.php or stored as a plugin (e.g. wp-content/mu-plugins/ff_debug.php).

downloadwrap
<?php
/*
Plugin Name: ff Debug
Plugin URI: http://oinkzwurgl.org/wordpress
Description: debugging facilities
Author: Philippe Kehl
Version: 1.0
Author URI: http://oinkzwurgl.org
*/

// PHP style security protection of some sort
defined('ABSPATH') or die('No script kiddies please!');

// initialise debugging facility early
add_action('init', 'ff_debug_init');
// http://codex.wordpress.org/Action_Reference#Actions_Run_During_a_Typical_Request

// initialise debugging, enable debugging if user is admin or a magic is defined
// (e.g. in wp-config.php) and supplied as URL parameter (?debug=<magic>)
function ff_debug_init()
{
    global $ff_debug_enabled;
    $ff_debug_enabled =
        (defined('FF_DEBUG_MAGIC') && ($_GET['debug'] == FF_DEBUG_MAGIC)) ||
        current_user_can('update_core') ? true : false;;

    DEBUG("ff_debug_init()");
   
    if ($ff_debug_enabled)
    {
        // something like Perl's 'use strict'...
        set_error_handler('ff_debug_errorhandler');
        error_reporting(E_STRICT);
        //error_reporting(E_NOTICE);
    }
}

// custom PHP error handler to debug use of uninitialised variables
function ff_debug_errorhandler($errno, $errstr, $errfile, $errline)
{
    if (($errno == E_NOTICE) && (strstr($errstr, "Undefined variable")))
    {
        DEBUG('%s:%s: %s', str_replace(pathinfo($_SERVER['SCRIPT_FILENAME'])['dirname'] . '/', '', $errfile),
              $errline, $errstr);
        return true;
    }
    //elseif (strstr($errstr, "Call to undefined function"))
    //{
    //    DEBUG('%s:%s: %s', str_replace(pathinfo($_SERVER['SCRIPT_FILENAME'])['dirname'] . '/', '', $errfile),
    //          $errline, $errstr);
    //    // FIXME: error_log($str) should work..
    //    return true;
    //}
    else
    {
        return false;
    }
}

// global debugging enabled/disabled flag
$ff_debug_enabled = true;

// returns true if debugging is enabled, false otherwise
function ff_debug_is_enabled()
{
    global $ff_debug_enabled;
    return $ff_debug_enabled;
}

// list of collected debug strings
$ff_debug_strings = array();

// add debug string, takes format string, stringifies complex objects (use %s)
function DEBUG()
{
    if (ff_debug_is_enabled())
    {
        $t = microtime(true);
        global $ff_debug_strings;
        $argv = func_get_args();
        $format = array_shift($argv);
        $fmtArgs = array();
        foreach ($argv as $arg)
        {
            if (is_array($arg) || is_object($arg))
            {
                array_push($fmtArgs, print_r($arg, true));
            }
            else
            {
                array_push($fmtArgs, $arg);
            }
        }

        $str = vsprintf($format, $fmtArgs);
        //$tsStr = strftime('%Y-%m-%d %H:%M:%S', $t)
        //    . sprintf('.%03d %+05.3f', ($t - floor($t)) * 1e3, $t - $_SERVER['REQUEST_TIME_FLOAT']);
        $tsStr = sprintf('%05.3f', $t - $_SERVER['REQUEST_TIME_FLOAT']);
        array_push($ff_debug_strings, esc_html($tsStr . ' ' . $str));
    }
}

// output debug stuff in footer
add_action('wp_footer', 'ff_debug_dump', 101);
//add_action('shutdown', function () { ff_debug_dump(true); }, 101);
function ff_debug_dump($inAdmin = false)
{
    if (ff_debug_is_enabled())
    {
        DEBUG("ff_debug_dump()");

        $style = $inAdmin ? ' style="clear: both; margin-left: 13em;"' : '';
        
        // add collected debug strings
        global $ff_debug_strings;
        if (count($ff_debug_strings))
        {
            echo('<pre class="debug"' . $style . '>' . join('<br/>', $ff_debug_strings) . '</pre>');
        }

        // attachments
        if (function_exists('ff_attachments'))
        {
            echo(ff_attachments(array('debug' => 1)));
        }
    }
}

?>

Source Code Snippets

The ffcode shortcode pulls the source code snippet from the custom field specified by the field parameter or from the enclosed content. Note, however, that the latter doesn't work very well. We can tell WordPress to not mess (a.k.a. texturize) with the contents (using the no_texturize_shortcodes filter), but the TinyMCE editor still will under some circumstances. YMMV.

downloadwrap
/* ***** [ffcode] source code inclusions and highlight *********************** */

add_shortcode('ffcode', 'ff_code');
function ff_code($args)
{
    // usage: [ffcode lang... field=...]
    //        [ffcode lang... filename=... wrap=0|1]...[/ffcode]
    // where field is the custom field name with the snippet

    $class = $args['lang'] ? 'lang-' . $args['lang'] : '';

    $code = '';

    // enclosed code
    if ($content)
    {
        $code = $content;
        $code = preg_replace('/^\s*<br\s*\/>\n/', '', $code);
        $code = preg_replace('/<br\s*\/>\n$/', '', $code);
    }
    // from custom field
    else
    {
        $cf = get_post_custom(get_the_ID());
        $code = $cf[ $args['field'] ][0];
        $code = str_replace(array('&',     '<',    '>'),
                            array('&amp;', '&lt;', '&gt;'),
                            $code);
    }
    
    return '<pre><code class="' . $class . '">' . $code . '</code></pre>';
}

// don't mess with $content (the TinyMCE editor still will, though..)
add_filter('no_texturize_shortcodes', 'ff_code_no_wptexturize');
function ff_code_no_wptexturize($shortcodes)
{
    $shortcodes[] = 'ffcode';
    return $shortcodes;
}

Add title Tooltips to wp_nav_menu() Items

downloadwrap
// add title (for fftooltip js) to nav menu items
add_filter('walker_nav_menu_start_el', 'ff_nav_menu_add_title', 10, 4);
function ff_nav_menu_add_title($item_output, $item, $depth, $args)
{
    $title = get_the_title($item->object_id);
    $res = preg_replace('/(<a)/', '<a title="' . esc_attr($title) . '" ', $item_output);
    return $res;
}

Remove Unused Entries From wp_nav_menu()

This filter removes all entries from the menu that are not part of the current page. I.e. if a top-level menu page is viewed it removes all sub-menu entries but that menu's children. If a sub-menu page is displayed it removes all other parent's sub-menu entries. See the example in the comments in the code below.

downloadwrap
// remove menu entries that are not interesting (by looking at the css classes)
add_filter('wp_nav_menu_objects', 'flipflip15_wp_nav_concise', null, 2);
function flipflip15_wp_nav_concise($items, $args)
{
    //DEBUG("id=%s", get_the_ID());
    //DEBUG("items=%s", $items);
    //DEBUG("items=%s", array_keys($items));
    //DEBUG("args=%s", $args);

    // e.g. all menus:
    //   menu1:
    //     top1 (top1_child1 top1_child2 ...)
    //     top2 (top2_child1 top2_child2 ...)
    //     top3 (top3_child1 top3_child2 ...)
    //   menu2:
    //     top4 (top4_child1 top4_child2 ...)
    //     top5 (top5_child1 top5_child2 ...)
    //
    // current page is top1, so we want this (case 2. for menu1, case 1. for menu2)
    //   menu1:
    //     top1 (top1_child1 top1_child2 ...)
    //     top2
    //     top3
    //   menu2:
    //     top4
    //     top5
    //
    // current page is top2_child1 (case 3. for menu1, case 1. for menu2)
    //   menu 1:
    //     top1
    //     top2 (top2_child1 top2_child2 ...)
    //     top3
    //   menu 2:
    //     top4
    //     top5
    
    // find 'current_page_item', or next closest ancestor (FIXME: does this always work?)
    $currentPageItem = null;
    foreach (array_values($items) as $page)
    {
        if ( $page->classes && (
            (array_search('current_page_item', $page->classes) !== false) ||
            (array_search('current-page-ancestor', $page->classes) !== false) ) )
        {
            $currentPageItem = $page;
            break;
        }
    }
    //DEBUG('currentPageItem=%s', $currentPageItem);

    // 1) current page is not part of this menu
    // --> remove all sub-menu items
    if ($currentPageItem == null)
    {    
        //DEBUG('other menu');
        foreach (array_keys($items) as $key)
        {
            if ($items[$key]->menu_item_parent != 0)
            {
                unset($items[$key]);
            }
        }
    }

    // 2) current page is in top-level menu
    // --> remove all sub-menu items that we're not the parent of
    else if ($currentPageItem->menu_item_parent == 0)
    {
        $parentId = $currentPageItem->ID;
        //DEBUG('top-level menu, id=%s', $parentId);
        foreach (array_keys($items) as $key)
        {
            if ( ($items[$key]->menu_item_parent != 0) && ($items[$key]->menu_item_parent != $parentId) )
            {
                unset($items[$key]);
            }
        }
    }

    // 3) current page is in sub-menu
    // --> remove all other parent's sub-menu entries
    else if ($currentPageItem->menu_item_parent != 0)
    {
        $parentId = $currentPageItem->menu_item_parent;
        //DEBUG('sub-menu, parent=%s', $parentId);
        foreach (array_keys($items) as $key)
        {
            if ( ($items[$key]->menu_item_parent != 0) && ($items[$key]->menu_item_parent != $parentId) )
            {
                unset($items[$key]);
            }
        }
    }

    // filter out items that point to pages the current user may not see
    $items = array_filter($items, function ($v)
    {
        $kind = '';
        $res = ff_may_display_post($v->object_id, $kind);
        if ($res && $kind) { array_push($v->classes, $kind); }
        return $res;
    });

    return $items;
}

Split out sub-menu <ul> from wp_nav_menu() Output

Transform the final HTML output so that the sub-menu <ul> list is not within the parent <li> but output after the top-level menu <ul>.

downloadwrap
// split out sub menu <ul>(s) and output two parts (one with the main menu and
// one with the sub-menu)
add_filter('wp_nav_menu', 'ff_wp_nav_split', null, 2);
function ff_wp_nav_split($html, $args)
{
    //DEBUG("html=%s", $html);
    $parts = preg_split('/(<ul +class="sub-menu">.*?)<\/ul>/s', $html, -1,
                        PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
    //DEBUG("parts=%s", $parts);
    $mainMenu = '';
    $subMenu = '';
    foreach ($parts as $part)
    {
        //DEBUG("part=%s", $part);
        if (strpos($part, 'sub-menu'))
        {
            $subMenu .= $part;
        }
        else
        {
            $mainMenu .= $part;
        }
    }
    //DEBUG("mainMenu=%s", $mainMenu);
    //DEBUG("subMenu=%s", $subMenu);

    return $mainMenu . "\n" . $subMenu;
}

Get Modified Time of the Last Significant Post Change

The get_the_modified_date() and get_the_modified_time() functions return the date and time of the last modification of a post or page. However, many operations that do not change the content, such as re-arranging the menu, update the post_modified timestamp. That's annoying.

Here's how I find the date/time of the last significant change by going back through the history (revisions) and finding where the content has changed last.

downloadwrap
    // get signifcant modification time (when the content changed last)
    // (because wp will touch the date also on meta data changes, menu reordering, etc.)
    $page = get_page($id);
    //DEBUG("%-20s %4d %s %s %s", $page->post_name, $page->ID, $page->post_date, $page->post_modified, md5($page->post_content));
    $revs = get_children(array(
        'post_parent'    => $id,
        'post_type'      => 'revision',
        'order'          => 'DESC',
        'orderby'        => 'post_date',
    ));
    foreach ($revs as $rev)
    {
        //DEBUG("%-20s %4d %s %s %s", $rev->post_name, $rev->ID, $rev->post_date, $rev->post_modified, md5($rev->post_content));
        if ($page->post_content != $rev->post_content)
        {
            break;
        }
        $page = $rev;

    }
    $mauthor = get_the_author_meta('nickname', $page->post_author);
    $mdate   = mysql2date(get_option('date_format'), $page->post_modified)
        . (flipflip15_isadmin() ? ' ' . mysql2date(get_option('time_format'), $page->post_modified) : '');

Note that this looks at the raw content, i.e. without any shortcodes expanded etc. E.g. this pages includes the code snippets from custom fields. So changes in those custom fields are changes the user sees. But they will not be reflected in the "updated ..." timestamp at the bottom of this page.