WordPress drill-down admin pages II

January 18, 2010

wordpress-logo-300x300I never did do a follow-up on the WordPress drill-down admin menus post. A lot of people continue to hit that article so I’ll take the time to give an example of how I easily resolved the issue.

The problem was that after an upgrade to WordPress Mu v 2.8.4a all my dynamically created, drill-down admin pages quit working because they now needed to be registered with WordPress for — completely understandable — security reasons.  As far as I can tell, the only way to register an admin page is through the menu system. Since I don’t want my drill down pages to be on the menu I needed a better way to do it.

One solution suggested was to just use the registered menu page as the callback to handle requests and that is for the most part what I did. The only issue with the solution is that the callback page has to also serve all the content since it can’t request an admin page that is not registered. Using one page to parse all requests and serve the appropriate content will get unwieldy for anything but the simplest of applications and if your application was that simple you wouldn’t be trying to figure out how to add custom drill-down admin menus.

I like modularity and easily managed code so my philosophy is that multiple small files are better than one huge file. I needed a way to solve the problem without breaking my philosophy or changing a lot of my existing code, which was already in multiple small files.

The idea is to build a container page to register on the admin menu and have the container page decide on which code to include based on analyzing the url. The included code can then handle and GET or POST requests and serve the appropriate page output.

<?php
/*
 * Descrition: gcgc admin page. This is the main container page
 * for all admin panels. All POSTS and GETS from all pages route
 * through this page and are handled by the appropriate included file.
*/
global $gcgc_Manager;
echo $gcgc_Manager->getHeader();
//analyze the url to determine which page to load

if (isset($_GET['panel'])){
    switch ($_GET['panel']){

    	case 'cache-admin' :
    	    include (dirname(__FILE__)."/gcgc-admin-cache.php");
    			break;

    	case 'vector-admin' :
           include (dirname(__FILE__)."/gcgc-admin-vector.php");
           break;

    	default :
           include (dirname(__FILE__)."/gcgc-admin-home.php");
    }
}else{
    include (dirname(__FILE__)."/gcgc-admin-home.php");
}

$gcgc_Manager->getFooter();
?>

I kept the above example brief but you can imagine that there can be an unlimited number of admin pages. And this way I didn’t have to change the included files at all — all I had to do was add the ‘&panel=’ parameter to existing request urls and problem solved.

  • Share/Bookmark

The Internet is my snippets database

January 3, 2010

Like everyone who codes for a living I will lift a snippet of code from a website. Usually it’s very generic code — as most code is — and I never give it a second thought. There are only so many ways to iterate an object. A good portion of my Internet code searching is to look for code I need not because I don’t know how to do it from scratch but because I don’t want to have to figure out how to do it from scratch in yet a new language.

Although there is no reason for most programmers to ever write a sort since sorting is built in to everything these days, a sort would be a good example. Back in the day a programmer would find him or herself needing to sort things all the time and you could not make a decent living typing out a brand spanking new quicksort every time you needed one. So you figured out how to write any particular sort only once per language and saved the code for later reuse.

A more modern example would be something like this:

    defaultHandler=function(defaults,params) {
      var i;
      for (i in params) {
          defaults[i]=params[i];
      }
      return defaults;
    }

This powerful little bit of Javascript can be dropped anywhere you need a list of default parameters to be merged with a list of user submitted settings. If you are being efficient about your work you are not going to re-type that code every time you use it; you are going to cut and paste it.

So, like many older professional coders, I have a database with reusable code snippets that I have built up over the course of more than two decades in the business. I have snippets for every sort algorithm you can name: the quicksort, heap sort, bubble sort, merge sort, selection sort, insertion sort ; name and address validators/formatters; date/timezone transformations; list processing; form validation; dynamic tables; and cool tricks. When I need to do something which I have done already a dozen times I simply copy and paste from my vast snippets database, never wasting time re-writing the same code more than once.

That was then; this is now. The problem is that I have all these snippets archived in @formula language, LotusScript, VB, and a bit of Javascript whereas today I need PHP, Perl, Java, and advanced Javascript frameworks like jQuery. I could find the code in my snippets database that does the same thing and then port it over to the new language but why do that when almost any code snippet can be found in under a minute with a deft Google search? I haven’t even used my snippets database in half a decade. The Internet is now my snippets database. I found the above Javascript snippet here, for example, while looking for some sample JavaScript to pass different object types as function parameters.

I haven’t carried a technical reference book to work in almost ten years. I don’t even use them at home anymore. The Internet is now my technical reference library.

  • Share/Bookmark

WordPress drill-down admin pages

October 8, 2009

I was gaining momentum on the Geocache Manager WordPress plugin last week after solving a couple of longstanding problems with unauthenticated Ajax calls and a few Javascript quirks with Internet Explorer when yet another pesky problem surfaces. When I say pesky it means that I have been fooling around with it for several days without zeroing in on a solution, thus making zero headway on the project. To make matters worse, the problem lies with a feature that has existed from the beginning and had worked until I upgraded from WordPress Mu 2.7 to 2.8.4a last week.

In the admin panel I have a custom top level menu with two entries: a page for the map management and a page for options. The following bit of code…

function addAdminMenu(){
	add_action('admin_menu', array(&$this, 'gcgc_plugin_menu'));
}

function gcgc_plugin_menu() {
	$gcgc_relpath  = str_replace('\\','/',dirname(__FILE__));
	$parent = $gcgc_relpath.'/gcgc-admin-form.php';
  	add_menu_page('Private Geocache Manager', 'Geocache', 1,
                                 $parent, array(&$this, 'gcgc_admin_page'));
  	add_submenu_page($parent, 'Geocache Options', 'Options', 1,
                                 'gcgc_options', array(&$this, 'gcgc_options_page'));
}

function gcgc_options_page(){
	$gcgc_relpath  = str_replace('\\','/',dirname(__FILE__));
	include($gcgc_relpath.'/gcgc-options-form.php');
}		

function gcgc_admin_page() {
	$gcgc_relpath  = str_replace('\\','/',dirname(__FILE__));
	include($gcgc_relpath.'/gcgc-admin-form.php');
}

…will give you a menu like so:
geocache-menu

That all works. The geocache admin form lists all the courses defined and for each course there is a link to load a new admin page for that course. This is basic functionality that has existed since the 1st week of development and suddenly the link no longer works. The link now generates a redirect to a sub-blog with a page not found. WTF! Where do you start at on that.

I admit I am a little weak on WordPress redirects and I am having a difficult time understanding why the redirect would go to a sub-blog in the WordPress Mu installation instead of the current blog. But aside from that I don’t understand why it started issuing a redirect in the first place when the code that generates the link hasn’t changed in like five months.

UPDATE: I fixed the redirect problem by deleting the sub-blogs. Now I am just getting a mangled url, like so:

http://gulfcoastgeocache.com/wp-admin/?c=1
So let's go over what I am thinking so far. I am of course using the wp-admin framework and have the faulty link set up similar to the pages accessed from the admin menu. The URL on the Geocache admin link is:
.../wp-admin/admin.php?page=private-geocache-manager/gcgc-admin-form.php
Then on that page there is an entry for each geocache course and each course has an anchor link to its own admin page, like so:
<a href=".../wp-admin/admin.php?
        page=private-geocache-manager/gcgc-admin-cache.php&id=20">
Geocache Name</a>

After checking the usual suspects -- verifying paths, searching for typos, confirming no function parameters had been inadvertently changed -- the only thing I can think of is that the drill-down admin page was working previously only due to a flaw in previous WordPress. I remember when putting it in that it seemed easy. Too easy.

Over the past few days I've done quite a bit of searching using every term anyone could probably think of having to do with registering an admin page that is not on a menu. Compared to other development platforms out there (Make no mistake, WordPress is a development platform) there just is not that much in-depth technical documentation outside of your standard stuff. There are many redundant articles on adding an admin menu, for example, but not much on adding a multi-page, drill-down admin system. You have to figure that out on your own. Either not many people are doing advanced development or they are very tight-lipped about their stuff.

This morning I finally came upon an article that at least verifies my thinking:

WordPress 2.8.1 contains changes to improve the security of plugins by ensuring that only correctly registered plugin pages can be accessed as well as only showing the link to the page to users who have the capability required in the add_x_page call.

The article makes no mention of dynamically created sub-pages accessed by means other than the WordPress menu structure but down in the comments I find a fellow traveler:

I already have add_action(’admin_menu’, ‘FUNCTION’); for my main settings page. However, there’s a plugin page which is not a menu link (but an inside plugin page which is referenced when the plugin settings are saved).

And the answer to my problem, if not the solution:

You cannot just link to pages that are not registered as this would make it easy to load up files which should not be loaded so you have two choices to fix your plugin.

He goes onto say that the workaround is to use a WordPress admin action hook or a call back to the main page to process the main form submissions.

I like to keep my pages small and manageable and this plugin will ultimately have half a dozen admin pages when all the features are added. As of now there are only three. But since I want many small files instead on one big file here is what I am going to do: I am going to create a main admin container page that includes the proper admin panel when loading based on the URL. Should have seen this problem coming and set up like this in the first place. Oh well. I'll report back when I get the changes complete.

Read the follow-up:

WordPress drill-down admin pages II

  • Share/Bookmark

WordPress custom templates and Javascript

September 27, 2009

Now that I have solved the recent unauthenticated Ajax problem in my Private Geocache Manager plugin, it’s time to move on to the second big problem that has plagued me for a while. I wasn’t even aware of it until I tried to demo the system to a user who worked at a place where Firefox isn’t available. I developed the plugin entirely in Firefox using only what I thought to be cross-platform utilities. The only thing that would even be close to non-standard code in the entire script library is jsfade, which itself is not complicated and which clearly takes into consideration the Internet Explorer quirks when performing the fade effect.

Nonetheless I still have a couple of Javascript issues outstanding which pretty much kill the whole deal since the Google Maps API is entirely a client side, Javascript environment and a big majority of folks still use Internet Explorer.

The first issue is the getElementById method somehow can’t figure out that the map canvas div element is in the DOM tree when it clearly is, throwing an ‘object expected’ error. I am accessing the elements from ajax calls so the entire page has loaded at the time of the calls so the div has obviously been declared by the time it is accessed. I think what I have to do here is check to see if the element is in the DOM tree before trying to access it. Firefox is smart enough to figure it out but I guess IE needs more explicit directions.

The second issue is that my ajax call is throwing the dreaded ‘unspecified error’. Debugging shows the error occurs at the mySACK.runAJAX() line. It is a simple ajax call using a standard implementation of the Simplified Ajax Code Kit, or SACK, and the code works perfectly in Firefox.

    function readMapData(arg) {
      mysack = new sack(ajaxurl); //wordpress admin url for ajax hook
      mysack.setVar('action', 'getVectorMarkers');
      mysack.execute = 0;
      mysack.method = 'POST';
      mysack.setVar('id', arg);
      mysack.onCompletion  = loadOverlay;
      mysack.onError = function() { alert('Ajax error ... '); };
      mysack.runAJAX();
      return true;
    }

The onCompletion callback never fires nor does the onError callback. Since I know there is not an error on the server side the next place I need to debug is inside the SACK ‘onCompletion’ code, which I’m not too crazy about. This is where you go, hmmm, maybe I should have just used jQuery for my Ajax in the first place.

So instead of editing a bunch of code that isn’t mine (the SACK wrapper class) I downloaded and installed the MS Script Editor and used it to find the exact bit of code throwing the error:

  if (this.method == "GET") {
     var totalurlstring = this.requestFile + "?" + this.URLString;
     this.xmlhttp.open(this.method, totalurlstring, true);
  }else{
      this.xmlhttp.open(this.method, this.requestFile, true);
  }

I’m using POST method so it’s the last line throwing the error. This.request is set to the parameter passed when creating the new sack object, in our case the wp_admin url, so the code would translate into

xmlhttp.open('POST', 'http://.../wp-admin/admin-ajax.php', true);

And that’s the benefit of writing these posts. Many times you figure out the problem by the time you finish describing it. When I went to copy the ajaxurl from the editor I noticed a typo in the URL which Firefox can obviously parse but IE can’t–I had the first pair of slashes escaped, eg http:///gulfcoastgeocache.com….

Grr….

So that still leaves me with the aggravating problem of getElementById not working properly. I’m debugging that now. Once I get that one sorted out I will be 90% complete with the plugin leaving only the initial setup script and the options page to finish.

  • Share/Bookmark

Viewer-facing ajax in WordPress plugin

September 3, 2009

It’s rare that I can’t solve a programming problem no matter how complex and thus I rarely leave a question on a support forum. I can’t even remember the last time; probably over a decade ago. Here is one I left recently at the supposedly fantastic WordPress.org plugin support forum.

I am writing a plugin to manage map resources and I having a couple a issues that I have been unable to figure out. I have a test site set up here –> http://gulfcoastgeocache.com/

I am using the Google Map API and lots of ajax both admin and viewer-facing to display custom maps. On the viewer facing side I am using SACK to call a small php script that in turn calls the actual function inside the plugin class.

$gcgc_path  = str_replace('\\','/',dirname(__FILE__));
require_once($gcgc_path.'/gcgcclass.php');
$gcgc_Manager->getVectorXML($_POST[id]);

The class function:

function getVectorXML(){
    header("Content-type: text/xml");
    $parms = array('id' => $_POST['id'],
                   'id_type' => 'vector_id'
                    );                         

    $cachets = $this->getVectorMapData($parms);
    $xhtml = "";
    $xhtml .= "";
    if($cachets){
      $xhtml .= "";
    	foreach ($cachets as $c){
    		$xhtml .= "";
    		}
    }
    $xhtml .= "";
    die($xhtml);
  }

Works fine if the user is logged in but is failing if user is not logged in. More accurately, the ajax round trip succeeds but my xml response is -1 if user is not logged in. No data is coming back.

The codex example for ajax does not illustrate a cookie being sent on the viewer-facing side but this is where I am thinking the problem lies, but really I am at a loss for an idea right now.

This is my first WP plugin so I may be having a noob moment. I’ve been revisiting this issue repeatedly over the past couple of months and, I hate to admit, I can’t figure it out. Any help would be greatly appreciated.

I posted that five days ago and not a peep. I even bumped it a couple of days ago. Still nothing. I’m not sure if the question is too stupid to warrant a response or if maybe it is too hard. I think the only way to get a quick response, or any response at all, for that matter, is to leave your query in broken English and to ask a question that indicates you don’t have a clue about what you are doing.

I’m not impressed. If anyone has a clue about the ajax, please let me know.

  • Share/Bookmark

DST

November 6, 2007

Everyone who works professionally with computers or software has at least heard about Daylight Saving Time issues and has likely grappled with them at one time or another. Incorrectly accounting for DST can wreak havoc with date sensitive processes like payroll, logging, and synchronizing systems.

Well, at the J.O.B. there was just such an occurrence relating to the daily time and attendance system software that computes time sheet hours for hourly workers based on a scheduled shift. The software was still using the old rules for DST; specifically the last Sunday of October instead of the first Sunday in November. Since the law changed the rules in 2005 and the rules went into affect this year, 2007, there have been numerous operating system and software patches to reflect the new rules. However, relying on the operating system patches to solve all your company’s DST issues will be a big mistake in almost every case as many programs were written by dumb asses who hard coded the DST rules into each and every application.

On my current job I am just a contractor who basically does whatever I am told, within reason. Even if I had the authority to fix such a problem before it occurred I would not have known where in the dozens and dozens of applications and millions of lines of code to look to even know about the problem. But when the graveyard shift is being paid for 13 hours instead 12 it suddenly becomes my problem and many people down in the cubes are satisfied to believe it is all my fault as well. I love being a contractor. Thick skin is definitely an asset.

Not wanting to go through the unpleasant experience ever again I tried to to come up with a way that the system would be configurable so that if or when the DST rules change again it will be as effortless as possible to make the changes to the code. I wrote the following and pasted it over three pages of existing code to compute whether a DST adjustment was needed. None of this ever  happens unless there is a Sunday involved.

First, set up some constants to represent the DST rules (yes, I am working in VB for now):

Const SPRING_FORWARD_MONTH = 3
Const SPRING_SUNDAY = 2
Const FALL_BACK_MONTH = 11
Const FALL_SUNDAY = 1
Const TIME_CHANGE_HOUR = 2

The above assumes the change will always be on a Sunday. Set the month, the Sunday (eg, 1st or 2nd etc), and the hour the time changes and the rest is handled computationally.

Private Function DSTFactorMins(ByVal inStartTime As Date, _
                              ByVal inEndTime As Date) As Integer

Dim vDayOfYear As Integer
Dim vEndingDayOfYear As Integer
Dim vStartingDayOfYear As Integer
Dim vSpringDay As Integer
Dim vFallDay As Integer
Dim vStartTime As Integer
Dim vEndTime As Integer
Dim adjType As String

vStartingDayOfYear = DatePart("y", inStartTime) 'y' parameter returns the day of year
vEndingDayOfYear = DatePart("y", inEndTime)
    'DST changes only affect those whose shift starts before the change and
    'ends after the change, ie, graveyard shift
If Weekday(inStartTime) = vbSaturday Then
        vStartTime = 0
Else
        vStartTime = DatePart("h", inStartTime)
End If

vEndTime = DatePart("h", inEndTime)

If (vStartTime < TIME_CHANGE_HOUR And TIME_CHANGE_HOUR <= vEndTime) Then

        If springForward(inStartTime) Or springForward(inEndTime) Then
                DSTFactorMins = -60 'minutes.
                adjType = "SPRING_FORWARD"
        ElseIf fallBack(inStartTime) Or fallBack(inEndTime) Then
                DSTFactorMins = 60 'minutes.
                adjType = "FALL_BACK"
        Else
                DSTFactorMins = 0
                adjType = ""
        End If

End If

Debug.Print "DST " & adjType & " adjustment is: " & Str$(DSTFactorMins)

End Function

Private Function springForward(ByVal inDate As Date) As Boolean
    wMonth = CInt(Format$(inDate, "mm"))
    springForward = False
    Dim vToday As Integer

    If wMonth = SPRING_FORWARD_MONTH Then
        vToday = DatePart("d", inDate)

        If vToday >= ((SPRING_SUNDAY - 1) * 7) And vToday <= (SPRING_SUNDAY * 7) Then
                springForward = True
        End If

    End If

End Function

Private Function fallBack(ByVal inDate As Date) As Boolean
    wMonth = CInt(Format$(inDate, "mm"))
    fallBack = False
    Dim vToday As Integer

    If wMonth = FALL_BACK_MONTH Then
            vToday = DatePart("d", inDate)

            If vToday >= ((FALL_SUNDAY - 1) * 7) And vToday <= (FALL_SUNDAY * 7) Then
                    fallBack = True
            End If

    End If

End Function

The obsessive compulsive programmer in me wanted to combine the fallBack and springForward functions but the lazy contractor decided not to. To Change back to the old DST rules just change the constants:

Const SPRING_FORWARD_MONTH = 4
Const SPRING_SUNDAY = 1
Const FALL_BACK_MONTH = 10
Const FALL_SUNDAY = 3
Const TIME_CHANGE_HOUR = 2

Unfortunately for this particular application the code is compiled, put on a server and run on a schedule. Ideally, a system profile should be used so that these types of changes can be made without altering the code at all, but I am only a system engineer, not God. I can only do so much. It took these guys well over a decade to get into the mess they are in and it can’t be fixed in a few months.

  • Share/Bookmark