IPBoard 3 - Providing a Value to a Setting During Hook Installation

Well, that title is a mouthful. It is intentionally verbose so that if anyone stumbles across this issue, they can easily find this article and not spend time (like I did) sifting through source code for an answer.

Background

Let's say you are creating a new hook in your development IPBoard 3.x 1. This hook changes the values of a few settings that already exist in the system (for example, disabling Gravatar support). While this setting could be easily changed by an administrator, we want to programmatically change it through the use of hooks.

During the hook export process, you tell IPBoard that you want to export the "Allow users to use Gravatar" setting. Yay! Automation is fun!

IPBoard provides you with the XML file and you now install it in your Production IPBoard environment. Everything installs correctly and IPBoard even says that 1 setting was changed.

You go to check that setting and it is still in its default position. What happened?

The First Problem

If you look into the hook XML file provided by IPBoard you will see the setting that will be changed upon installation. Here is what it will look like:

<hookextras_setting>
  <setting>
    <conf_id>282</conf_id>
    <conf_title>Allow users to use Gravatars</conf_title>
    <conf_description><![CDATA[<a href='http://en.gravatar.com/' target='_blank'>Click here for more information on Gravatars</a>]]></conf_description>
    <conf_group>30</conf_group>
    <conf_type>yes_no</conf_type>
    <conf_key>allow_gravatars</conf_key>
    <conf_value/>
    <conf_default>1</conf_default>
    <conf_extra/>
    <conf_evalphp/>
    <conf_protected>1</conf_protected>
    <conf_position>5</conf_position>
    <conf_start_group/>
    <conf_add_cache>1</conf_add_cache>
    <conf_keywords/>
    <conf_title_keyword>userprofiles</conf_title_keyword>
    <conf_is_title>0</conf_is_title>
  </setting>
</hookextras_settings>

Notice that <conf_value/> field? It is currently set to nothing, which means IPBoard will take the default value instead. By why didn't IPBoard provide us with a value, even though the resulting XML shows a field for it? Let's look at the code:

// Line 1180: admin/applications/core/modules_admin/applications/hooks.php
if( count($_settings) )
{
  # Now get the group data for the XML file
  $this->DB->build( array( 'select' => '*', 'from' => 'core_sys_conf_settings', 'where' => "conf_key IN('" . implode( "','", $_settings ) . "')" ) );
  $this->DB->execute();
  
  while( $r = $this->DB->fetch() )
  {
    $r['conf_value']      = '';
    $r['conf_title_keyword']  = $titles[ $r['conf_group'] ];
    $r['conf_is_title']     = 0;

    $xml->addElementAsRecord( 'hookextras_settings', 'setting', $r );
  }
}

What is happening here is that IPBoard grabs all the settings that we want to export and then, while looping through each one, it blanks out the $r['conf_value'] value, then sends the whole lot to the XML file. That's why the XML file has a <conf_value/> tag.

Solution Becomes Problem 2

It could be that IPBoard wants to prevent leaking sensitive configuration data to the exported XML hook file, so it blanks out the value of every exported setting. That seems logical.

You think you're smart so you simply open the XML file in a text editor and change <conf_value/> to <conf_value>0</conf_value>, thereby saying that we are going to disable Gravatars. Easy peasy, right?

You reinstall the hook and you find that the setting is still in its default value. Now we find that IPBoard isn't respecting what the XML file is telling it to do. Let's dig into the source code again.

Starting on line 618 in admin/applications/core/modules_admin/applications/hooks.php in the install_hook function, we see this:

foreach( $xml->fetchElements('hookextras_settings') as $node )
{
  foreach( $xml->fetchElements('setting', $node) as $_setting )
  {
    $setting  = $xml->fetchElementsFromRecord( $_setting );
  
    if( $setting['conf_is_title'] == 1)
    {
      $settingGroups[] = array(
        'conf_title_title'   => $setting['conf_title_title'],
        'conf_title_desc'    => $setting['conf_title_desc'],
        'conf_title_noshow'  => $setting['conf_title_noshow'],
        'conf_title_keyword' => $setting['conf_title_keyword'],
        'conf_title_app'     => $setting['conf_title_app'],
        'conf_title_tab'     => $setting['conf_title_tab'],
      );
    }
    else
    {
      $settings[] = array(
        'conf_title'          => $setting['conf_title'],
        'conf_description'    => $setting['conf_description'],
        'conf_group'          => $setting['conf_group'],
        'conf_type'           => $setting['conf_type'],
        'conf_key'            => $setting['conf_key'],
        'conf_default'        => $setting['conf_default'],
        'conf_extra'          => $setting['conf_extra'],
        'conf_evalphp'        => $setting['conf_evalphp'],
        'conf_protected'      => $setting['conf_protected'],
        'conf_position'       => $setting['conf_position'],
        'conf_start_group'    => $setting['conf_start_group'],
        'conf_add_cache'      => $setting['conf_add_cache'],
        'conf_title_keyword'  => $setting['conf_title_keyword'],
      );
    }
  }
}

This is a lot of code but bear with me. We want to review the else branch of the conditional because that it the code that changes a setting, versus the other branch which changes a setting group (the container for settings).

We can see that various setting parameters are changed, even the default value of the setting, but nowhere is the conf_value ever set, nor is the <conf_value> tag ever loaded from the XML file.

Real Solution

The only real solution is, when exporting the hook, issue a database query that updates the setting directly in the database. You can provide the export process with an original field value so that the setting can be reverted when the hook is uninstalled.

Conclusion

(╯°□°)╯︵ ┻━┻

Footnotes

  1. The 3.x version is important here. IPBoard 4.x has been completely rewritten so this article won't work for upgraded boards. Hopefully the bug will be fixed by then.