The following post is a tutorial on writing new hacks for the 6.6 control panel. At the end there is a brief set of instructions that will walk you through installing a new page.
Even if you don't understand all of the concepts the code uses (references and the like), please try to follow the way things work.
Please post any questions you have, I'll be glad to answer'em.
-------------------------------------
The main new feature in UBB.classic 6.6 is the new control panel. The UI was designed to mimic the look and feel of the new UBB.x and Eve platform control panels - a menu on the left, DHTML tabs on the top, with a dash of DHTML magic where appropriate.
As with very many "second hand" features (such as the Avatar implementation taken right from UBB.x), I had the advantage of knowing all of the downsides and potential gotchas of the DHTML tab system before implementing it. Knowing the potential problems lead me to a single conclusion - the existing control panel code could not be smartly adapted to the new UI.
The old control panel code made too many assumptions about how navigation worked (it didn't), which elements of which pages belonged where, etc, etc. Adapting it to think how it needed to think would have been just as much work as creating new code.
Before adding new code or areas, it is critical that you understand how the control panel thinks and works.
Each area of the control panel is a
page. Each page has one or more
tabs. A request to cp.cgi?
page=
primary;
tab=
email would go to a page called "primary" and a tab called "email."
The page and tab determine which sets of libraries and templates get loaded. Let's walk through the above request to cp.cgi.
After authentication, the menu definition for your user status is loaded. There are different menus for admins, moderators, and users that have yet to log in. If the requested page is not available in the menu, an error is thrown. If the page is available, a check is made for a page subroutine with the passed page name - "cp2_page_
primary" in this case. If the subroutine exists, it's executed and takes over. If not, a check for the page and tab is made: "cp2_page_
primary_tab_
email"
If NEITHER subroutine was found, checks are made for template files for that page and tab. In this case, the two templates would be "cp2_
primary" and "cp2_
primary_tab_
email." If they are found, they are loaded. If there are multiple tab templates, only the main template and the one tab we're looking for are loaded. In our case, let's assume that only cp2_primary exists.
Each template contains at least three subroutines. Examples in this case would be "cp2_template_
primary_
init" "cp2_template_
primary_
libs" and "cp2_template_
primary_
run"
The
libs subroutine is expected to return a list of libraries required to run the page in question. For our example, the
libs subroutines returns a one element array, containing "cp2_vars", meaning cp2_vars.cgi is to be loaded.
After the
libs subroutine is executed, the
init subroutine is expected.
Init may perform any additional tasks that need to be run when the template loads.
The
run subroutine is expected to return an array containing strings of HTML - one element per page tab. A
run subroutine that generates a page with three tabs would return three elements in the array.
Back to the task at hand. Once the template is loaded (and any libraries required are loaded), another check is made for a page subroutine or the page/tab subroutine. The page code should be in the loaded libraries. In our case, the cp2_vars.cgi file contains our page subroutine.
When the proper subroutine is found, it's called. If no subroutines could be found at this point, an error is thrown.
That brings us to the basic assembly of page routines.
Each page subroutine normally has four distinct parts:
sub cp2_page_primary {
# Part 1: Setup
if($in{d} eq "submit") {
# Part 2: Form submission. This may be passed off to another subroutine.
&GetOrPost("POST");
@example = split(/n/, $in{new_example});
&WriteFileAsArray("example.cgi", @example);
$in{returnpage} = $in{page};
$in{returntab} = 0;
&StandardHTML("Done.", { back_button => -2 });
} # end if
# Part 3: Form preparation
my @example_file = &OpenFileAsArray("example.cgi");
# Part 4: Print the form
print &WrapContent(
&WrapTabSet(
{},
[&cp2_template_primary_run(@example_file)],
undef, undef, [],
)
);
exit;
} # end cp2_page_primary
The code in part 1 is always executed when the page is called, even when it's a form being processed. This is the place to check arguments, load code or data, etc.
Part 2 is generally an if block that checks to see if this is a form submission, or what kind of form submission is happening. Determining the state of a form is usually left to the "d" paramater to the script. A value of "submit" is used to denote a normal submit action, while other values can be specified by the form you create. The exact methodology is left up to you. Some current pages use "setter" instead of "d".
The end of part 2 is generally a call to StandardHTML with a confirmation message. More information on the new options that StandardHTML can take is below. If the form submit was a partial success or a failure, you can either throw StandardHTML or continue running and pass an error out as part of the normal page content. See sub AutoVars and the real cp2_page_primary in cp2_vars.cgi for example code.
If this isn't a form submit, or there were errors during the form submit and you didn't throw StandardHTML, the code in part 3 is run.
Finally, we print the form and exit. This is done by calling two subroutines.
&WrapContent returns the passed HTML attached to the standard control panel header and footer.
&WrapTabSet is more interesting. It takes five possible arguments:
- A hashref with arguments for a page form (see code for calling method)
- An arrayref, each element containing the HTML for one tab on the page
- A string with an "override" page name for the menus and breadcrumbs
- A string with an "override" tab name
- An arrayref that contains arrayrefs that contain hashrefs that define any lower tabs that may be required on each tab.
That last argument has probably confused you.
Actions to be performed on the task at hand are attached to each tab using tabs at the BOTTOM. These bottom tabs are defined on a tab-by-tab basis, meaning DHTML tab 1 may have a different set of bottom tabs than DHTML tab 2, if at all. Thus, to get tabs at the bottom of the second DHTML tab, but not the first or third, the following arrayref would be passed:
[
[], # first tab
[
{
name => "Bottom Tab",
url => "$vars_config{CGIURL}/$CONTROLPANEL",
},
], # second tab
[], # third tab
];
There may be a maximum of three bottom tabs per DHTML tab. Sizes are fixed to about 30%, so be careful about wording.
The last three arguments to &WrapTabSet are optional and may be excluded entirely. You will often see short calls that only define the first two arguments:
print &WrapContent(&WrapTabSet({}, [&cp2_page_whatever_run()]));
The first argument to &WrapTabSet can also be a point of confusion.
Forms can apply to the page in two ways. First, a form may submit everything on the page, meaning that everything in every tab belongs to the same form. Second, a form may submit only everything on that one tab. In the second mode, each individual tab would have its own form.
The form arugments to &WrapTabSet create page-wide forms. To create tab-specific forms, use the "formstart" and "formend" components (via &MakeComponentHTML, which we'll learn about shortly).
This is a good place to talk about the new arguments to StandardHTML. Beyond the message to be posted, StandardHTML can take any of the following arguments in a hashref:
- redirect_title - combine with redirect_url to create a forwarding page... title of "Continuing..." link
- redirect_url - URL that link returns to / URL to forward to, if redirect_title is used
- redirect_delay - seconds to wait if this is a redirect page (redirect_title and redirect_url passed)
- override - force a page name (for tab only - does NOT change navigation)
- overtab - force a tab name
- back_button - title for the "Return" link... use w/ redirect_url for messages rather than forwards,
or alone without a redirect_url for wording alone...
... *OR* one of three numbers:
- 0 - do not display "return" link/title
- -1 - return to control panel home
- -2 - attempt to determine the page and tab we were on using magic vars created by
a page form. Set $in{returnpage} and $in{returntab} manually if a page form wasn't used.
[*]extra_qs - hashref of additional arguments for return link, if back_button is -2
When back_button is passed -2, StandardHTML blindly assumes that the current request came from a page based form rather than a tab based form. The page based form includes two hidden fields - "returnpage" and "returntab" - that are generated based on the currently selected tab. These hidden fields are not created when using tab based forms. To "smartly" use -2 after a tab based form submit, you must set them yourself (either in the form or before calling StandardHTML).
(Yes, this all seems very obtuse... The prototype code ended up working well enough that it turned into the real thing far before I realized what was happening. Oops. Apologies for the stupidity. At least it works and I can still hack at it.
)
Okay, so in review of what we have so far...
- Menu sets determine what users can get to
- All accessable areas are in PAGES and TABS
- Pages and tabs are executed via library code
- Library code is loaded by the templates
- Templates (and thus library code) are loaded on demand based on the requesting page
- There's a generic model for a page handling sub that can be easily cloned
There are two remaining general areas that you need to know about. First, you need to know how to actually make the HTML that gets stuck into each tab on the page.
Above I refered to a subroutine named MakeComponentHTML. &MakeComponentHTML itself takes two arguments: a hashref containing a list of &Template substitutions (for replacing %%TEMPLATE%% strings in the resulting HTML), and an arrayref containing hashrefs that build the actual HTML.
I don't recall actually ever using the &Template hashref. Most calls you'll see to &MakeComponentHTML look something like:
return &MakeComponentHTML({}, [
{
type => "header",
title => "Hello, world!",
},
]);
Each of the hashrefs has to have a "type" key with a valid component type. Available types are:
Listables:
----------
-checkbox
-multichecks
-radio
-selectlist
-selectlistplustext
Text Input:
----------
-regfieldselect
-text
-textarea
Other Form Components:
----------
-confirmprompt
-customtitles
-datebox
-formend
-formstart
-hiddenfield
Expandy Bits:
----------
-expandybuffer
-expandycheckbox
-expandyheader
Other:
----------
-generic
-halfandhalf
-header
-raw
-twocellwrapper
There are some generally common arguments (name, title, class/classleft/classright). All listables can take an options argument that contains the list of available options (i.e. a list of forums as generated by &MakeForumSelectList).
Other than those noted, EACH COMPONENT HAS ITS OWN INDIVIDUAL ARGUMENT SET THAT MAY NOT MAKE SENSE WHEN COMPARED TO OTHERS OF A SIMILAR TYPE. Such problems are evolutionary quirks and will be corrected with time.
Let's create a simple form.
return &MakeComponentHTML({}, [
{
type => "formstart",
hiddens => [ # Yes, it's ugly. Sorry.
{ name => "page", value => $in{page} },
{ name => "tab", value => $in{tab} },
{ name => "d", value => "submit" },
],
},
{
type => "header",
level => 1, # 1 == blue, 2 == grey, 3/4 == alt colors
bold => 1,
title => "Example Page Header",
},
{
type => "text",
name => "test_box",
title => "Example Text Box",
desc => "Put something in the box.",
},
{
type => "formend",
submit_text => "Test the form",
},
]);
This code will be inserted into a single tab on a single page. The tab will have one blue header row ("Example Page Header"), a text box titled "Example Text Box", and a submit button saying "Test the form"
You can easily create your own component for &MakeComponentHTML, if you wish. For a type named "example", create a subroutine called &MakeComponentHTML_example. Many templates define their own components. You should model the interface for any custom components after existing components. See "selectlist" and "selectlistplustext" as examples. If you're only going to use a custom component in one template, it's safe to put the subroutine there rather than in cp2_common.
While it's good practice to use the components as much as you can, you don't really have to do so. Your template run subroutine can easily return raw HTML, or you can use the raw component type.
So, now you can put together a page, set up a form handler, and "make things go" in general.
But nothing will work unless the page can be found in the menus. Editing the menus is the last thing you'll need to know.
The menus are stored at the bottom of cp2_lib.
The menus are composed of a set of references stored in a hash. Among them are:
GENERIC_MENUS_HOMEPAGE
GENERIC_MENUS_TIER_ONE
GENERIC_MENUS_TIER_TWO
MOD_MENUS_HOMEPAGE
MOD_MENUS_TIER_ONE
MOD_MENUS_TIER_TWO
ADMIN_MENUS_HOMEPAGE
ADMIN_MENUS_TIER_ONE
ADMIN_MENUS_TIER_TWO
The generic set is loaded when there is no user logged in. The mod set is loaded when the user is a moderator. The admin set is loaded when the user is an administrator.
"homepage" is the name of the page that users receive when the breadcrumbs are used / when the user first logs in.
The first tier of menus produces the list of items on the left of the screen. It's an array reference that contains array references. The first item in any such array is the category title. All others are the names of the items in that category. For instance:
$tier_one = [
["Category One", "item", "item2", "item3"],
["Cat2", "item4"],
];
The first tier menu is purely cosmetic. Valid pages may exist that are not within that menu.
The second tier menu defines each individual page and the tabs on the page.
An example page:
$tier_two = {
item => {
title => "Name of Page",
hidden => 0,
nolink => 0,
menu_override => "",
extraqs => [],
tabs => [],
},
};
Only title and tabs are required. title is the actual title of the page (as will appear on the menu and in the breadcrumbs). tabs is an arrayref of the tabs on the page.
If hidden is set to 1, the menu item will not show up in the left menu, no matter what.
If nolink is set to 1, the main page will not be displayed as a breadcrumb. This can be combined with menu_override to make pages that "think" they're really somewhere else in the menu system.
If menu_override is set to a valid page name, all navigation on the page will be borrowed from that page instead. This trick is used to create stand alone tabbed pages that really belong to another page, such as the style editor and user profile editor.
extraqs contains an arrayref listing any form arguments that need to be passed into the query string when StandardHTML is called and back_button is -2. This is again part of the trickery used to make pages look like they belong somewhere else (as part of the user editor - you can't edit a user profile unless you know which profile to load).
Finally, tabs is an array reference containing the list of tabs on this page.
my $tabs = [
{
id => "normal_tab",
title => "Normal Tab Title",
type => "tab",
},
{
id => "standalone_tab",
title => "Standalone Tab",
type => "standalone",
},
{
id => "anti_tab",
title => "Standalone Tab, Really!",
type => "tab",
},
{
id => "url",
title => "Click me to go somewhere...",
type => "url",
url => "http://...",
},
];
Tabs are pretty straightforward. Each tab must have three elements - the id, title, and type. The id is the name for the tab as given in URLs. The title is the name printed on the tab itself.
There are four possible tab types:
Type "tab" signifies a normal DHTML tab.
Type "standalone" is a special HTML tab. Standalone tab contents are not loaded with the other DHTML tabs. When the tab grippy is clicked, the new page loads with only that one tab. Users can click on any other DHTML or standalone tabs from any page, and will be taken to that tab, even if the page has to reload. Note that form changes will be lost during the page transition.
Type "standalonenotab" is like a standalone tab (it loads as a separate page). However, it is not a normal tab. When this tab has focus, all other tabs are hidden. This allows for multiple "pages" without needing to define real live pages.
Type "url" requires a url paramater with the tab. When the tab is clicked, the user is sent to the specified URL.
Now that I've told you all of this, it's time to make your first control panel hack!
If you haven't done so already, go grab the latest 6.6+ release (Beta Release 1 at time of writing) and install it. Let's call our new page
test.
First, start with the template. Create a new file named
cp2_test.pl. Put the following inside of it:
# Test page template for 6.6 control panel
sub cp2_template_test_init {}
sub cp2_template_test_libs {}
sub cp2_template_test_run {
return &MakeComponentHTML([
{
type => "formstart",
hiddens => [
{ name => "page", value => "test" },
{ name => "d", value => "submit" },
],
},
{
type => "header",
level => 1,
bold => 1,
title => "Test Page Header",
},
{
type => "text",
name => "test_box",
title => "Example Text Box",
desc => "Put something in the box.",
},
{
type => "formend",
submit_text => "Test the form",
},
]);
} # end cp2_template_test_run
sub cp2_page_test {
if($in{d} eq "submit") {
&StandardHTML("Thank you. You said $in{test_box}");
} # end if
print &WrapContent(&WrapTabSet({}, [&cp2_template_test_run]));
exit;
} # end cp2_page_test
1;
Save the file and upload it to your Templates directory. Don't forget the 1 at the end!
Notice that we're cheating! The page subroutine has been put inside the template itself. If you have lots of code, you'll want to use an external library file and use the libs subroutine to load it. For such a small snippet of code, however, putting the page subroutine in the template is OK.
Now that we have the template and the page sub, we can add the test page to the menu. Open up cp2_lib.cgi and search for "ADMIN_MENUS_TIER_TWO". There will be two instances. You'll want the second one, around line 1,200.
Right below the line containing "ADMIN_MENUS_TIER_TWO", insert the following:
test => {
title => "Test Page",
tabs => [{
id => "test",
title => "Test Page",
type => "tab",
}],
},
Now, look a few lines up. You'll see a block starting with "ADMIN_MENUS_TIER_ONE" that looks something like:
ADMIN_MENUS_TIER_ONE => [
[$vars_...
[$vars_...
[$vars_...
[$vars_...
[$vars_...
[$vars_...
[$vars_...
],
You'll want to add one more to the bottom, so it will look something like:
[$vars_...
[$vars_...
["Test Header", "test"],
],
Save cp2_lib.cgi, then log into the control panel as an admin. If everything went right, you should see a new heading at the bottom of the control panel menu titled "Test Header". It will contain exactly one item, "Test Page". When clicking on Test Page, you should get the test page form that we've set up, and when you submit that form, you should be told what you submitted.
If you get an internal server error or an uncaught exception, you probably made a typo. Try copying and pasting right from this document instead of typing things in by hand. Be careful not to break the first tier menus when copying and pasting.
If you've made it this far, congrats! You've successfully installed your first 6.6 control panel hack!
Your next challenge will be writing your own code. That should be fun.