Redmine shows 404 error on opening GIT repositories after update

September 1st, 2016

We run Redmine together with Gitolite and view the repositories within Redmine. After an update of Redmine and GIT some repositories would randomly show a 404 error or only display one of many branches.

Redmine retrieves the GIT log by calling "git --git-dir DIRECTORY log". I found out under which user Passenger runs and called this command under this user. It showed "fatal: failed to read object [...]: Permission denied" or "fatal: bad default revision 'HEAD'", so I was sure that there is a permission issue. I changed the permissions on the repository recursively. It's also necessary to change the UMASK of Gitolite in this case. It's in .gitolite.rc. This file has obviously been overwritten by the update of Gitolite.

Drupal 7: How to clean up and update a really f***ed up installation

August 11th, 2016

Drupal is quite involved and reading manuals is hard for some people. So they put non-core modules into the modules folder. They hack core files. They hack modules. They hack themes. And worst of all: They are unable to document their whatever-you-wanna-call-it. A customer came back to us to update his Drupal 7 site. The people who created the website were not available - guess why. So here is what we did.

Set Up Update Workflow
We had to re-start the whole update process many times, because we broke the installation often. So we documented every single step in a step-to-step document. We used a drush dump from the live site and deployed it on a test hosting.

Move Modules to Correct Place
In our case, there were non-core modules in the core modules folder. Some modules were present twice, once in the modules folder and once in sites/all/modules. Both of them in different versions of course. Drush cannot handle that mess. It assumes every non-core module in the sites folder and overwrites updated modules there. On updating a site, this behavior leads to duplicate modules. Furthermore, a core update breaks everything, because drush overwrites the core modules folder so many modules are missing afterwards.

We downloaded Drupal to compile a list of standard core modules. We compared the list against the list of modules present in our core modules folder and built the complement. We deactivated all modules in that list, moved the modules over to the correct folder and re-enabled them. We found that some of the modules were duplicates. We looked at the activated versions and only copied the modules over that were really enabled. We deleted remaining folders and enabled all modules again.

drush dis -y NON-CORE-MODULES-IN-CORE-FOLDER
rm DUPLICATE-MODULES
mv NON-CORE-MODULES-IN-CORE-FOLDER sites/all/modules
drush en -y NON-CORE-MODULES-IN-CORE-FOLDER

Get drush up Working
drush up gave us a headache on this site. The most problematic things were:

  • Duplicate project keys. This site uses the community theme and the dropletz_helper_tags module. We removed the project = "omega" and roject = "shortcode" from their respective info file to be able to proceed with drush up.
  • The error "The MODULE-NAME directory could not be found within the modules directory at..." regularly occured. It was usually connected to duplicate modules of which the duplicate was removed from the core modules folder like described above. Disabling and re-enabling the module and Cache-Clears helped sometimes. Maybe drush up just wants to be called more than once.
  • Remove symbolic links from the drush archive-dump target folder. Otherwise it fails.

Handle Hacked Modules
After a successful drush up we tested the site with the customer. There was one error causing a 500. It was connected to a code error in a media module. We found that at least one of its dependencies was updated so that a function was missing there. We compared the module to its original and found that there are too many changes. We decided to keep it and all its dependencies at the old version. Thus, we added a step to our update document that disables these modules, moves the folders away from the Drupal root and moves and re-activates them after the update.

For every graphical deviation from the live site, we looked at deviating CSS and HTML. We found several hacked modules and re-applied the hacks to the updated modules. We documented them so that we can get rid of them soon or are at least able to re-apply them after the next update.

Test, Test, Test
We found that the .htaccess file was changed and overriden by the core update. So we included it in our update list. Our list also contains the change of the memcache key, since it causes problems when running another site on the same server using the same key.

Drupal 7, Multistep Webforms with Webservice Calls: Best Practices

August 8th, 2016

We use a multistep webform in Drupal for a complex insurance application form. Between some steps, it is necessary to call a webservice (German insurance "standard" BiPro - implement once, customize everywhere). The webservice can refuse to insure the applicant or it can break due to various technical reasons. In both cases, it is necessary to inform the user that he or she cannot proceed and the reason for it.

Use Custom Validation to Call Webservices
We found that the best place to call the webservice is hook_form_validate. It is called first after each step is submitted and can prevent the user to proceed by setting error messages. hook_form_alter is called subsequently and does (or should) not change the user flow anymore.

Via hook_form_alter, we add a custom validation function to our webform:

$form['#validate'][] = 'example_form_validate';

In the custom validation function, we (1) access submitted values, (2) access submitted values from previous steps, (3) modify default values, (4) call webservices, and (5) set error messages.

function example_form_validate($form, &$form_state) {
  if ($form_state['webform']['page_num'] == 1) {
    // In order to access previously submitted data by machine name, we
    // generate a translation array machine_name => component_id.
    $formassignment = array();
    foreach ($form['#node']->webform['components'] as $cid => $tmpArr) {
      $formassignment[$tmpArr['form_key']] = $cid;
    }

    $token = $form_state['storage']['submitted'] \\
            [$formassignment['token']]; (2)
    if (!$token) {
      $ret = $ws->run(); // (call webservice) (4)
      $form_state['values']['submitted']['token'] = $token; // (3)
      if ($ret->status == 'NOK') {
        form_set_error('submitted][ws_error_1', \\
          'A technical error has occured. Please call 12345.'); // (5)
      }
    }

    $beginn = $form_state['values']['submitted']['beginn']; // (1)
  }
}

In each step that calls a webservice after submit, we add a text field error_ws_PAGENUM right at the bottom. We hide it with CSS. The code above sets it to an error (note the quirky, but necessary syntax 'submitted][ws_error_1').

Note that we set default values of some fields. These fields don't need to be placed in the same step that is submitted. All values of all fields are available in hook_form_alter in $form_state['values']['submitted'], independent of when and how they were added to the form.

Use Machine Names instead of Component IDs
Note the differences in accessing the currently submitted values (1) in $form_state['values']['submitted'] and the "old" values from storage (2) in $form_state['storage']['submitted']. The latter ones can only be accessed using their corresponding component ID (cid).

In order to have the webforms in our GIT repository, we export and save them to text files with the standard export function of webforms. When importing the forms, the cids may change. This is why we exclusively work with machine names in the code. The previously submitted values are only available using their cids though. This is why we translate machine names to cids in the variable $formassignment and access the values by $form_state['storage']['submitted'][$formassignment['MACHINE-NAME']];

Support Testing with Code Prefill
Since our form has many steps with many fields, it is cumbersome to fill in required fields during development and testing. Thus, we set some configuration values on a dedicated module setup page.

example.module:

function example_form_alter(&$form, &$form_state, $form_id) {
  if ($form_state['webform']['page_num'] == 1) {
    $fill_fields = variable_get('example_ola_prefill', false);
    if ($fill_fields) {
      $form['submitted']['beginn']['#default_value'] = '2016-09-01';
      // ...
    }
  }  
}

function example_menu() {
  $items = array();
  $items['admin/config/system/example'] = array(
          'title' => 'Example',
          'description' => 'Example',
          'page callback' => 'drupal_get_form',
          'page arguments' => array('example_admin'),
          'access arguments' => array('administer site configuration'),
          'type' => MENU_NORMAL_ITEM,
          'file' => 'example.admin.inc'
      );
  return $items;
}

example.admin.inc:

function example_admin($form, &$form_state) {
  $form['example_ola_prefill'] = array(
      '#type' => 'checkbox',
      '#title' => 'Test mode: prefill fields',
      '#default_value' => variable_get('example_ola_prefill', false),
      '#description' => t(''),
  );

  return system_settings_form($form);
}

Don't Panic on White Screen of Death
During development, the form can get into inconsistent states, especially when changing fields while filling in a form in a browser window. This can lead to funny behaviour or even a white screen. It can be mitigated by deleting the last partial result on the webform page (/node/NODE-ID/webform-results).

Furthermore, cache-clears help a lot. When working on CSS and JS, it's sufficient to clear the css-js cache and go back and forth one step to reload. There is no need to completely restart the form in this case.

Use XDebug
Drupal works perfectly well with XDebug. We use Netbeans to develop. Netbeans integrates with XDebug easily. It allows us to set breakpoints and inspect variable states in the form.

Don't use HTML in Labels and Options
We tried to get markup into webform labels and select options. It doesn't work. There is a module that claims to enable this possibility, but it's broken. There are various approaches with custom text filters, but none of them work with a current Drupal 7 and Webforms 4. We are not sure if there are filters involved here at all. Instead, we use markup fields, style them to act as labels and use JavaScript for more sophisticated options.

Don't mess with Frontend Validation
We use clientside validation with webforms. And we use jQuery which interacts with fields. We tried to copy values from some source fields to some destination fields and make the destination read-only. That results in validation errors on submit. We didn't find any reliable solution for this. One reliable workaround is to synchronize values between two fields. This way, the destination field can stay writable.

$("#destination").val($("#source").val());
$("#destination").keyup(function(){
  $("#source").val($(this).val());
});
$("#source").keyup(function(){
  $("#destination").val($(this).val());
});

In our case it was necessary to prefill a required date field with a value from a previous step and make it read-only only when a certain selection is made in another select field. The webform frontend validation showed validation errors in this case. We worked around that issue by deactivating the required date field with conditions.

OwnCloud: Ungültiger privater Schlüssel

Juli 8th, 2016

Wenn man die Verschlüsselung und einen Authentifizierungsserver in OwnCloud nutzt und das Passwort extern verändert bekommt man in der Weboberfläche die Fehlermeldung "Ungültiger privater Schlüssel für die Verschlüsselungs-App. Bitte aktualisieren Sie Ihr privates Schlüsselpasswort, um den Zugriff auf Ihre verschlüsselten Dateien wiederherzustellen."

Im eigenen Profil muss man das alte und dann das neue Passwort eingeben, damit OwnCloud die Dateien mit dem alten Schlüssel ent- und mit dem neuen Schlüssel wieder verschlüsseln kann:

ECL: How to cast a custom DATASET to a SET OF an atomic type

Juni 30th, 2016

I wanted to use the function

Std.Str.combinewords(r, ',');

and found, that it only takes SET OF STRING as input. I had a dataset of a record type containing one string with the name "word". The solution is to call the SET function on the dataset (t in this example):

SET OF STRING r := SET(t, word);