<?php

/**
 * @file
 * The core module for managing LOM-CH descriptions.
 *
 * The Archibald module allows editors to manage LOM-CH descriptions, and
 * publish them to the national catalog of the Swiss digital school library.
 * Descriptions are defined as nodes, of the archibald_lomch_description type.
 * Descriptions are tied to a "partner" (previously known as "content partner"),
 * which are defined as custom entities.
 *
 * Each user that helps with the creation of a description (be it the author or
 * users that make editions) has a personal VCard, attached to her account.
 * These VCards will also be sent to the national catalog. VCards are stored as
 * custom entities as well.
 *
 * We make heavy use of the Revisioning module to handle our "workflow". It is
 * thanks to this module that we can easily manage the "pending" and "published"
 * states.
 */

define('ARCHIBALD_CATALOG_PARTNER', 'own');
define('ARCHIBALD_CATALOG_NATIONAL', 'national');
define('ARCHIBALD_CATALOG_BOTH', 'own,national');
define('ARCHIBALD_REVISION_STATE_NOT_PUBLISHED', 'nothing published');
define('ARCHIBALD_REVISION_STATE_NEWER_PUBLISHED', 'newer revision published');
define('ARCHIBALD_REVISION_STATE_OLDER_PUBLISHED', 'older revision published');
define('ARCHIBALD_REVISION_STATE_CURRENT', 'currently published');
define('ARCHIBALD_REVISION_STATE_CURRENT_NEW_DRAFT', 'currently published, new draft pending');
define('ARCHIBALD_ENCRYPTION_CIPHER', 'AES-256-CBC');

/**
 * Implements hook_menu().
 */
function archibald_menu() {
  return array(
    // Base entry for all Archibald admin UIs.
    'admin/archibald' => array(
      'title' => 'Archibald',
      'description' => 'Manage Archibald descriptions and VCards.',
      'access arguments' => array('access administration pages'),
      'page callback' => 'system_admin_menu_block_page',
      'file' => 'system.admin.inc',
      'file path' => drupal_get_path('module', 'system'),
    ),
    'admin/archibald/import-vocabularies/%' => array(
      // As this page can be called directly after the installation, the user
      // may not have any of our custom permissions yet. But, if the user has
      // access to administer the site, it is safe to assume she can import a
      // few vocabularies. This is why we don't use the 'administer archibald'
      // permission here.
      'access arguments' => array('administer site configuration'),
      'page callback' => 'archibald_batch_import_vocabularies_trigger',
      'page arguments' => array(3),
      'file' => 'includes/archibald.admin.inc',
      'type' => MENU_CALLBACK,
    ),
    // This entry just exists so we have a valid entry in the administration
    // menus, but it simply proxies to the "Pending" page.
    'admin/archibald/description' => array(
      'title callback' => '_archibald_title_callback',
      'title arguments' => array('Descriptions', array(), array('context' => 'archibald:views')),
      'description' => 'Manage LOM-CH descriptions.',
      'access callback' => 'archibald_views_access',
      'access arguments' => array('pending'),
      'page callback' => 'drupal_goto',
      'page arguments' => array('admin/archibald/description/pending'),
    ),
    'admin/config/services/archibald' => array(
      'title' => 'Archibald',
      'description' => 'Configure Archibald settings and connection with the dsb ReST API.',
      'access arguments' => array('administer archibald'),
      'page callback' => 'drupal_get_form',
      'page arguments' => array('archibald_admin_settings_form'),
      'file' => 'includes/archibald.admin.inc',
    ),
    'archibald/vcard/%' => array(
      'title' => 'VCard',
      'page callback' => 'archibald_vcard_view',
      'page arguments' => array(2),
      'access callback' => 'archibald_vcard_access',
      'access arguments' => array('view', 2),
    ),
    'archibald/partner/%' => array(
      'title' => 'Partner',
      'page callback' => 'archibald_partner_page_view',
      'page arguments' => array(2),
      'access callback' => 'archibald_partner_access',
      'access arguments' => array('view', 2),
    ),
    'node/%node/json' => array(
      'title' => 'JSON',
      'access callback' => 'archibald_json_access',
      'access arguments' => array(1),
      'page callback' => 'archibald_page_node_json_view',
      'page arguments' => array(1),
      'type' => MENU_LOCAL_TASK,
      'weight' => 100,
    ),
    'node/%node/validate' => array(
      'title' => 'Validate',
      'access callback' => 'node_access',
      'access arguments' => array('update', 1),
      'page callback' => 'archibald_page_node_validation_view',
      'page arguments' => array(1),
      'type' => MENU_LOCAL_TASK,
      'weight' => 90,
    ),
  );
}

/**
 * Implements hook_menu_alter().
 */
function archibald_menu_alter(&$items) {
  // We have some issues with the Entity module's automatic entity titles. It
  // calls strtolower() on the entity type, which breaks translations in German.
  // Fix it and force a certain title.
  $items['admin/archibald/partner/add']['title'] = 'Add partner';
  $items['admin/archibald/partner/add']['page callback'] = 'archibald_custom_entity_add_page';
  unset(
    $items['admin/archibald/partner/add']['title callback'],
    $items['admin/archibald/partner/add']['title arguments']
  );
  $items['admin/archibald/vcard/add']['title'] = 'Add VCard';
  $items['admin/archibald/vcard/add']['page callback'] = 'archibald_custom_entity_add_page';
  unset(
    $items['admin/archibald/vcard/add']['title callback'],
    $items['admin/archibald/vcard/add']['title arguments']
  );

  // We cannot configure our default views to be visible for certain users
  // based on a role, but we cannot rely on a single permission either. This is
  // why we alter the menu items for our default views. We provide our own
  // access callback, which will verify multiple permissions, so we can grant
  // access in an intuitive way.
  foreach (array(
    'admin/archibald/description/proposals',
    'admin/archibald/description/pending',
    'admin/archibald/description/published',
    'admin/archibald/description/republish',
  ) as $path) {
    if (isset($items[$path])) {
      $items[$path]['access callback'] = 'archibald_views_access';
      // Pass the display name as the argument for the access callback.
      $items[$path]['access arguments'] = array($items[$path]['load arguments'][1]);

      // Change the parent title callback. We want to use t(), but with a
      // specific context. This is necessary, because the dsb vocabulary is not
      // necessarily the same in each language. For example, in French,
      // "Description" translates as "Notice" instead of "Description". We
      // cannot use the default title callback. If Drupal sees the callback is
      // t(), it passes the title arguments as the 2nd argument, which is not
      // enough, as context information has to passed as the 3rd argument. This
      // is why we use a custom title callback, which just proxies to t().
      // That way, we can use the "title arguments" as they are meant to be
      // used.
      $items[$path]['title callback'] = '_archibald_title_callback';
      $items[$path]['title arguments'] = array(
        $items[$path]['title'],
        array(),
        array('context' => 'archibald:views'),
      );
    }
  }

  // Alter the View and Edit node tabs. See
  // archibald_revisioning_view_edit_access_callback() for more information on
  // why this is necessary.
  $items['node/%node']['access callback'] = 'archibald_revisioning_view_edit_access_callback';
  $items['node/%node/edit']['access callback'] = 'archibald_revisioning_view_edit_access_callback';
}

/**
 * Title callback.
 *
 * @see archibald_menu_alter()
 */
function _archibald_title_callback() {
  return call_user_func_array('t', func_get_args());
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
function archibald_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  // On our description listing pages, add a link for creating new descriptions.
  if (preg_match('/^admin\/archibald\/description(\/?|$)/', $root_path)) {
    $item = menu_get_item('node/add/archibald-lomch-description');
    if ($item['access']) {
      $item['title'] = t("Add description");
      $data['actions']['output'][] = array(
        '#theme' => 'menu_local_action',
        '#link' => $item,
      );
    }
  }
}

/**
 * Implements hook_theme().
 */
function archibald_theme() {
  $path = drupal_get_path('module', 'archibald');

  $base = array(
    'file' => 'theme.inc',
    'path' => "$path/theme",
  );

  return array(
    'archibald_partner_summary' => array(
      'variables' => array(
        'entity' => NULL,
        'logo_image_style' => 'archibald_partner_logo_summary',
      ),
      'template' => 'archibald_partner--summary',
      'preprocess functions' => array(
        'archibald_preprocess_archibald_partner',
      ),
    ) + $base,
    'archibald_partner_default' => array(
      'variables' => array(
        'entity' => NULL,
        'logo_image_style' => 'archibald_partner_logo_default',
      ),
      'template' => 'archibald_partner--default',
      'preprocess functions' => array(
        'archibald_preprocess_archibald_partner',
      ),
    ) + $base,
    'archibald_preview_image_formatter' => array(
      'variables' => array('item' => NULL, 'image_style' => NULL, 'path' => NULL),
    ) + $base,
  );
}

/**
 * Implements hook_block_info().
 */
function archibald_block_info() {
  return array(
    'info' => array(
      'info' => t("Archibald: information links"),
      'cache' => DRUPAL_CACHE_GLOBAL,
      // By default, set this block as available for the Dashboard module.
      'status' => module_exists('dashboard'),
      'region' => module_exists('dashboard') ? 'dashboard_inactive' : NULL,
    ),
    'admin_links' => array(
      'info' => t("Archibald: admin links"),
      'cache' => DRUPAL_CACHE_PER_ROLE,
      'properties' => array(
        'administrative' => TRUE,
      ),
      // By default, set this block as available for the Dashboard module.
      'status' => module_exists('dashboard'),
      'region' => module_exists('dashboard') ? 'dashboard_inactive' : NULL,
    ),
  );
}

/**
 * Implements hook_block_info_alter().
 */
function archibald_block_info_alter(&$blocks) {
  // We want to alter the following blocks:
  foreach (array(
    'archibald_partners-block',
    // See https://www.drupal.org/node/346602 for more information on why we
    // have to hash here.
    md5('archibald_lomch_descriptions-proposals_block'),
    md5('archibald_lomch_descriptions-pending_block'),
    md5('archibald_lomch_descriptions-republish_block'),
  ) as $delta) {
    if (isset($blocks['views'][$delta])) {
      // By default, set this block as available for the Dashboard module.
      $blocks['views'][$delta]['status'] = 1;
      $blocks['views'][$delta]['region'] = 'dashboard_inactive';
    }
  }
}

/**
 * Implements hook_block_view().
 */
function archibald_block_view($delta) {
  switch ($delta) {
    case 'info':
      return array(
        'subject' => t("Information links", array(), array('context' => 'archibald:block')),
        'content' => array(
          '#theme' => 'links',
          '#links' => array(
            array(
              'title' => t("Ontology server", array(), array('context' => 'archibald:block')),
              'href' => 'http://ontology.biblio.educa.ch/',
              'attributes' => array('target' => '_blank'),
            ),
            array(
              'title' => t("LOM-CHv1.1 standard (PDF)", array(), array('context' => 'archibald:block')),
              'href' => 'https://dsb-api.educa.ch/lom-ch/LOM-CHv1.1_de.pdf',
              'attributes' => array('target' => '_blank'),
            ),
            array(
              'title' => t("LOM-CHv1.2 standard (PDF)", array(), array('context' => 'archibald:block')),
              'href' => 'https://dsb-api.educa.ch/lom-ch/LOM-CHv1.2_de.pdf',
              'attributes' => array('target' => '_blank'),
            ),
          ),
        ),
      );

    case 'admin_links':
      $links = array();
      foreach (array(
        'admin/config/services/archibald' => t("Configure Archibald", array(), array('context' => 'archibald:block')),
        'admin/archibald/vcard' => t("Manage VCards", array(), array('context' => 'archibald:block')),
        'admin/archibald/description' => t("Manage descriptions", array(), array('context' => 'archibald:block')),
        'admin/structure/taxonomy/archibald_lomch_description_licenses' => t("Manage licenses", array(), array('context' => 'archibald:block')),
        'admin/structure/taxonomy/archibald_lomch_coverage' => t("Manage coverage", array(), array('context' => 'archibald:block')),
        'http://localhost:8080/admin/structure/taxonomy/archibald_lomch_description_languages' => t("Manage resource languages", array(), array('context' => 'archibald:block')),
      ) as $path => $title) {
        $item = menu_get_item($path);
        if ($item['access']) {
          $links[] = array(
            'title' => $title,
            'href' => $path,
          );
        }
      }

      return array(
        'subject' => t("Shortcuts", array(), array('context' => 'archibald:block')),
        'content' => array(
          '#theme' => 'links',
          '#links' => $links,
        ),
      );
      break;
  }
}

/**
 * Implements hook_theme_registry_alter().
 */
function archibald_theme_registry_alter(&$registry) {
  $path = drupal_get_path('module', 'archibald');

  // We sometimes need to render the required marker slightly differently based
  // on certain field settings. The chances of colliding with another module or
  // theme are very, very small, so we can take the risk of altering the theme
  // registry for the required marker.
  $registry['form_required_marker']['function'] = 'theme_archibald_form_required_marker';
  $registry['form_required_marker']['includes'][] = "$path/theme/theme.inc";
}

/**
 * Implements hook_permission().
 */
function archibald_permission() {
  return array(
    'administer archibald' => array(
      'title' => t('Administer the Archibald module settings'),
    ),
    'view archibald_vcards' => array(
      'title' => t('View the VCards'),
    ),
    'view archibald_partners' => array(
      'title' => t('View the Partners'),
    ),
    'administer archibald_vcards' => array(
      'title' => t('Administer the VCards'),
    ),
    'administer archibald_partners' => array(
      'title' => t('Administer the Partners'),
    ),
  );
}

/**
 * Implements hook_init().
 *
 * @see includes/archibald.entities.inc
 */
function archibald_init() {
  // This is a hack for adding fields to the custom archibald_vcard and
  // archibald_partner entity types. We cannot do this on install, as at that
  // time the entities are not defined yet. We also use this opportunity to add
  // fields to the user entity. We put it in here, meaning we actually add the
  // fields on the first page load. We set a system variable when done, so we
  // only do this once.
  if (!variable_get('archibald_entities_added_fields', 0)) {
    module_load_include('inc', 'archibald', 'includes/archibald.entities');
    archibald_vcard_entity_insert();
    archibald_partner_entity_insert();
    archibald_user_entity_insert();
    archibald_taxonomy_entity_insert();
    variable_set('archibald_entities_added_fields', 1);
  }
}

/**
 * Implements hook_node_info().
 */
function archibald_node_info() {
  return array(
    'archibald_lomch_description' => array(
      'name' => t("Description", array(), array('context' => 'archibald:node')),
      'base' => 'archibald_lomch_description',
      'description' => t("A <em>LOM-CH</em> description. Descriptions contain meta-data describing online educational resources."),
      'has_title' => TRUE,
      'title_label' => t("Title"),
      'locked' => TRUE,
    ),
  );
}

/**
 * Implements hook_hook_info().
 */
function archibald_hook_info() {
  $hooks = array();
  foreach (array(
    'archibald_json_export_alter',
    'archibald_language_fallback_list_alter',
  ) as $hook) {
    $hooks[$hook] = array(
      'group' => 'archibald',
    );
  }
  return $hooks;
}

/**
 * Implements hook_entity_info().
 *
 * @see includes/archibald.entities.inc
 */
function archibald_entity_info() {
  return array(
    'archibald_vcard' => array(
      'label' => t('VCard'),
      'base table' => 'archibald_vcard',
      'entity keys' => array(
        'id' => 'id',
        'label' => 'name',
      ),
      'entity class' => 'ArchibaldVcardEntity',
      'controller class' => 'EntityAPIController',
      'views controller class' => 'EntityDefaultViewsController',
      'devel contrib generator class' => 'ArchibaldVcardEntityGenerator',
      'access callback' => 'archibald_vcard_access',
      'uri callback' => 'entity_class_uri',
      'admin ui' => array(
        'path' => 'admin/archibald/vcard',
        'controller class' => 'EntityDefaultUIController',
      ),
      'fieldable' => TRUE,
      'bundles' => array(
        'archibald_vcard' => array(
          'label' => t('VCard'),
          'admin' => array(
            'path' => 'admin/archibald/vcard',
          ),
        ),
      ),
      'module' => 'archibald',
    ),
    'archibald_partner' => array(
      'label' => t('Partner'),
      'base table' => 'archibald_partner',
      'entity keys' => array(
        'id' => 'id',
        'label' => 'name',
      ),
      'entity class' => 'ArchibaldPartnerEntity',
      'controller class' => 'EntityAPIController',
      'views controller class' => 'EntityDefaultViewsController',
      'access callback' => 'archibald_partner_access',
      'uri callback' => 'entity_class_uri',
      'admin ui' => array(
        'path' => 'admin/archibald/partner',
        'controller class' => 'EntityDefaultUIController',
      ),
      'fieldable' => TRUE,
      'bundles' => array(
        'archibald_partner' => array(
          'label' => t('Partner'),
          'admin' => array(
            'path' => 'admin/archibald/partner',
          ),
        ),
      ),
      'module' => 'archibald',
    ),
  );
}

/**
 * Implements hook_entity_property_info().
 */
function archibald_entity_property_info() {
  // We provide property info for the archibald_partner is_default property.
  // This allows the Entity API to expose it in Views as a boolean field
  // (instead of simply a text field). However, if we describe one property, we
  // have to describe them all... and we have to describe them for all custom
  // entities... So:
  return array(
    'archibald_vcard' => array(
      'properties' => array(
        'id' => array(
          'label' => t("ID"),
          'type' => 'integer',
          'schema field' => 'id',
        ),
        'name' => array(
          'label' => t("Label"),
          'type' => 'text',
          'schema field' => 'name',
        ),
      ),
    ),
    'archibald_partner' => array(
      'properties' => array(
        'id' => array(
          'label' => t("ID"),
          'type' => 'integer',
          'schema field' => 'id',
        ),
        'name' => array(
          'label' => t("Label"),
          'type' => 'text',
          'schema field' => 'name',
        ),
        'is_default' => array(
          'label' => t("Is default"),
          'type' => 'boolean',
          'schema field' => 'is_default',
        ),
      ),
    ),
  );
}

/**
 * Implements hook_cron_queue_info().
 */
function archibald_cron_queue_info() {
  return array(
    'archibald_queue' => array(
      'worker callback' => 'archibald_queue_worker',
    )
  );
}

/**
 * Implements hook_libraries_info().
 */
function archibald_libraries_info() {
  return array(
    'dsb-client' => array(
      'name' => 'dsb Client',
      '_archibald version' => '>=0.21.0',
      '_archibald check class' => 'Educa\DSB\Client\ApiClient\ClientV2',
      'vendor url' => 'http://org.educa.ch/',
      'download url' => 'https://github.com/educach/dsb-client/releases',
      'drush download url' => 'https://github.com/educach/dsb-client/archive/0.21.0.zip',
      'version callback' => 'archibald_get_library_version',
      'path' => 'src',
      'xautoload' => function($adapter) {
        $adapter->composerJson('composer.json');
      },
    ),
    'sabre-xml' => array(
      'name' => 'Sabre XML',
      '_archibald version' => '>=1.5.0',
      '_archibald check class' => 'Sabre\Xml\Reader',
      'vendor url' => 'http://sabre.io/xml/',
      'download url' => 'https://github.com/fruux/sabre-xml/releases',
      'drush download url' => 'https://github.com/fruux/sabre-xml/archive/1.5.0.zip',
      'version callback' => 'archibald_get_library_version',
      'path' => 'src',
      'xautoload' => function($adapter) {
        $adapter->composerJson('composer.json');
      },
    ),
    'sabre-uri' => array(
      'name' => 'Sabre URI',
      '_archibald version' => '>=1.2.1',
      '_archibald check class' => 'Sabre\Xml\Version',
      'vendor url' => 'http://sabre.io/uri/',
      'download url' => 'https://github.com/fruux/sabre-uri/releases',
      'drush download url' => 'https://github.com/fruux/sabre-uri/archive/1.2.1.zip',
      'version callback' => 'archibald_get_library_version',
      'path' => 'src',
      'xautoload' => function($adapter) {
        $adapter->composerJson('composer.json');
      },
    ),
    'guzzle' => array(
      'name' => 'Guzzle',
      '_archibald version' => '>=6.2.3',
      '_archibald check class' => 'GuzzleHttp\Client',
      'vendor url' => 'http://guzzlephp.org/',
      'download url' => 'https://github.com/guzzle/guzzle/releases',
      'drush download url' => 'https://github.com/guzzle/guzzle/archive/6.2.3.zip',
      'version callback' => 'archibald_get_library_version',
      'path' => 'src',
      'xautoload' => function($adapter) {
        $adapter->composerJson('composer.json');
      },
    ),
    'psr7' => array(
      'name' => 'Psr7',
      '_archibald version' => '>=1.4.2',
      '_archibald check class' => 'GuzzleHttp\Psr7\Request',
      'vendor url' => 'http://guzzlephp.org/',
      'download url' => 'https://github.com/guzzle/psr7/releases',
      'drush download url' => 'https://github.com/guzzle/psr7/archive/1.4.2.zip',
      'version callback' => 'archibald_get_library_version',
      'path' => 'src',
      'xautoload' => function($adapter) {
        $adapter->composerJson('composer.json');
      },
    ),
    'http-message' => array(
      'name' => 'Http Message',
      '_archibald version' => '>=1.0.1',
      '_archibald check interface' => 'Psr\Http\Message\MessageInterface',
      'vendor url' => 'http://www.php-fig.org/',
      'download url' => 'https://github.com/php-fig/http-message/releases',
      'drush download url' => 'https://github.com/php-fig/http-message/archive/1.0.1.zip',
      'version callback' => 'archibald_get_library_version',
      'path' => 'src',
      'xautoload' => function($adapter) {
        $adapter->composerJson('composer.json');
      },
    ),
    'promises' => array(
      'name' => 'Promises',
      '_archibald version' => '>=1.3.1',
      '_archibald check class' => 'GuzzleHttp\Promise\TaskQueue',
      'vendor url' => 'http://guzzlephp.org/',
      'download url' => 'https://github.com/guzzle/promises/releases',
      'drush download url' => 'https://github.com/guzzle/promises/archive/v1.3.1.zip',
      'version callback' => 'archibald_get_library_version',
      'path' => 'src',
      'xautoload' => function($adapter) {
        $adapter->composerJson('composer.json');
      },
    ),
    'vcard' => array(
      'name' => 'JeroenDesloovere\VCard',
      '_archibald version' => '>=1.3.0',
      '_archibald check class' => 'JeroenDesloovere\VCard\VCard',
      'vendor url' => 'https://github.com/jeroendesloovere/vcard',
      'download url' => 'https://github.com/jeroendesloovere/vcard/releases',
      'drush download url' => 'https://github.com/jeroendesloovere/vcard/archive/1.3.0.zip',
      'version callback' => 'archibald_get_library_version',
      'path' => 'src',
      'xautoload' => function($adapter) {
        $adapter->composerJson('composer.json');
      },
    ),
    'Transliterator' => array(
      'name' => 'Behat\Transliterator',
      '_archibald version' => '>=1.2.0',
      '_archibald check class' => 'Behat\Transliterator\Transliterator',
      'vendor url' => 'https://github.com/Behat/Transliterator',
      'download url' => 'https://github.com/Behat/Transliterator/releases',
      'drush download url' => 'https://github.com/Behat/Transliterator/archive/v1.2.0.zip',
      'version callback' => 'archibald_get_library_version',
      'path' => 'src',
      'xautoload' => function($adapter) {
        $adapter->composerJson('composer.json');
      },
    ),
  );
}

/**
 * Implements hook_node_type_insert().
 *
 * @see archibald_lomch_description_node_type_insert()
 * @see includes/archibald.entities.inc
 */
function archibald_node_type_insert($content_type) {
  module_load_include('inc', 'archibald', 'includes/archibald.entities');

  if ($content_type->type == 'archibald_lomch_description') {
    archibald_lomch_description_node_type_insert();
  }
}

/**
 * Implements hook_form() for archibald_lomch_description.
 *
 * Form callback for archibald_lomch_description content type.
 */
function archibald_lomch_description_form($node, $form_state) {
  return node_content_form($node, $form_state);
}

/**
 * Implements hook_view() for archibald_lomch_description.
 */
function archibald_lomch_description_view($node, $view_mode, $langcode = NULL) {
  $path = drupal_get_path('module', 'archibald');
  drupal_add_css("$path/css/archibald.lomch_description_node_view.css");

  // Do we have to display a "view on portal" link?
  if (
    variable_get('archibald_show_link_to_dsb_portal', FALSE) &&
    $node->status == NODE_PUBLISHED &&
    !empty($node->lom_id)
  ) {
    $node->content['archibald_view_on_portal'] = array(
      '#type' => 'container',
      '#attributes' => array('class' => array('archibald-node-view-on-portal-link')),
      '#weight' => -20,
      'value' => array(
        '#markup' => l(
          t("view on portal", array(), array('context' => 'archibald:node_view')),
          str_replace('[ID]', urlencode($node->lom_id), variable_get('archibald_dsb_portal_url', '')),
          array('attributes' => array(
            'target' => '_blank',
          ))
        ),
      ),
    );
  }

  // Prepare the meta data fieldset.
  $node->content['archibald_metadata'] = array(
    '#type' => 'fieldset',
    '#title' => t("Meta-Metadata"),
    // #collapsed and #collapsible don't work outside of a form context.
    // See https://www.drupal.org/node/1431054#comment-6830210.
    '#attributes' => array('class' => array('collapsible', 'collapsed', 'archibald-node-meta-data')),
    '#attached' => array('js' => array('misc/collapse.js', 'misc/form.js')),
    '#weight' => -10,
  );

  // Display the revisioning status.
  switch (archibald_get_revision_status($node)) {
    case ARCHIBALD_REVISION_STATE_NOT_PUBLISHED:
      $status = t("No published revision");
      break;

    case ARCHIBALD_REVISION_STATE_NEWER_PUBLISHED:
      $status = t("A newer revision is currently published");
      break;

    case ARCHIBALD_REVISION_STATE_OLDER_PUBLISHED:
      $status = t("An older revision is currently published");
      break;

    case ARCHIBALD_REVISION_STATE_CURRENT:
      $status = t("This is the currently published revision");
      break;

    case ARCHIBALD_REVISION_STATE_CURRENT_NEW_DRAFT:
      $status = t("This is the currently published revision, but a newer revision is currently in draft");
      break;

    default:
      $status = '-';
      break;

  }
  $node->content['archibald_metadata']['revisioning_status'] = array(
    '#type' => 'container',
    '#attributes' => array('class' => array('archibald-node-meta-data__section', 'archibald-node-meta-data__section--revisioning-status')),
    'label' => array(
      '#theme' => 'html_tag',
      '#tag' => 'label',
      '#value' => t("Publication status") . ':',
    ),
    'value' => array(
      '#markup' => $status,
    ),
  );

  // Display the meta-metadata schema. Currently, we only publish LOM-CHv1.3.
  $node->content['archibald_metadata']['metametadata_schema'] = array(
    '#type' => 'container',
    '#attributes' => array('class' => array('archibald-node-meta-data__section', 'archibald-node-meta-data__section--meta-meta-data-schema')),
    'label' => array(
      '#theme' => 'html_tag',
      '#tag' => 'label',
      '#value' => t("Metametadata schema") . ':',
    ),
    'value' => array(
      '#markup' => 'LOM-CHv1.3',
    ),
  );

  // Display the LOM ID, if any.
  $node->content['archibald_metadata']['lom_id'] = array(
    '#type' => 'container',
    '#attributes' => array('class' => array('archibald-node-meta-data__section', 'archibald-node-meta-data__section--lom-id')),
    'label' => array(
      '#theme' => 'html_tag',
      '#tag' => 'label',
      '#value' => t("ID") . ':',
    ),
    'value' => array(
      '#markup' => isset($node->lom_id) ? check_plain($node->lom_id) : '-',
    ),
  );

  // Display the partner data.
  if (isset($node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id'])) {
    $partner = archibald_partner_load($node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id']);
  }

  $node->content['archibald_metadata']['partner_data'] = array(
    '#type' => 'container',
    '#attributes' => array('class' => array('archibald-node-meta-data__section', 'archibald-node-meta-data__section--partner-data')),
    'label' => array(
      '#theme' => 'html_tag',
      '#tag' => 'label',
      '#value' => t("Partner") . ':',
    ),
    'value' => array(
      '#markup' => isset($partner) ? theme('archibald_partner_summary', array('entity' => $partner)) : t("No partner set"),
    ),
  );

  // Load contributor data (the VCard, if available; user data if not).
  $contributors = array();
  $result = db_select('node_revision', 'v')
              ->fields('v', array('uid'))
              ->condition('v.nid', $node->nid)
              ->distinct()
              ->execute();
  while ($uid = $result->fetchField()) {
    $account = user_load($uid);
    if (!empty($account->user_vcard[LANGUAGE_NONE][0]['target_id'])) {
      $vcard = archibald_vcard_load($account->user_vcard[LANGUAGE_NONE][0]['target_id']);
      $contributors[$uid] = user_access('administer archibald_vcards') ?
        l($vcard->name, "admin/archibald/vcard/manage/{$vcard->id}", array(
          'query' => array('destination' => "node/{$node->nid}"),
        )) :
        check_plain($vcard->name);
    }
    else {
      $string = t("%name (has no VCard)", array(
        '%name' => $account->name,
      ));
      $contributors[$uid] = user_access('administer users') ?
        l($string, "user/$uid/edit", array(
          'query' => array('destination' => "node/{$node->nid}"),
          'html' => TRUE,
        )) :
        $string;
    }
  }

  $node->content['archibald_metadata']['contributors'] = array(
    '#type' => 'container',
    '#attributes' => array('class' => array('archibald-node-meta-data__section', 'archibald-node-meta-data__section--contributors')),
    'label' => array(
      '#theme' => 'html_tag',
      '#tag' => 'label',
      '#value' => t("Contributors") . ':',
    ),
    'value' => array(
      '#theme' => 'table',
      '#header' => array(t("Name"), t("Role")),
      '#rows' => array_map(function($item) {
        // Currently, LOM-CHv1.2 only has "creators". In future version, there
        // will be "editors" and "validators" as well.
        return array($item, t("creator", array(), array('context' => 'archibald:node_view')));
      }, $contributors),
    ),
  );

  return $node;
}

/**
 * Implements hook_node_view().
 */
function archibald_node_view($node, $view_mode, $langcode = NULL) {
  // Only apply to archibald_lomch_description nodes.
  if ($node->type == 'archibald_lomch_description') {
    // We want to slightly alter the default way preview images are displayed.
    // We cannot do this in archibald_lomch_description_view(), because the
    // field data is not yet prepared for rendering.
    if (!empty($node->content['lomch_preview_image'])) {
      foreach ($node->content['lomch_preview_image'] as $key => $value) {
        if (is_numeric($key)) {
          $node->content['lomch_preview_image'][$key]['#theme'] = 'archibald_preview_image_formatter';
        }
      }
    }

    // Locations can be formatted as URLs. Check all of them. If there are items
    // that are URLs, treat them as links.
    if (!empty($node->content['lomch_location'])) {
      foreach ($node->content['lomch_location'] as $key => $value) {
        if (
          is_numeric($key) &&
          (
            valid_url($node->content['lomch_location'][$key]['#markup']) ||
            valid_url($node->content['lomch_location'][$key]['#markup'], TRUE)
          )
        ) {
          $node->content['lomch_location'][$key]['#markup'] = l(
            $node->content['lomch_location'][$key]['#markup'],
            (preg_match('/^https?:\/\//', $node->content['lomch_location'][$key]['#markup']) ?
              '' :
              'http://'
            ) . $node->content['lomch_location'][$key]['#markup'],
            array('attributes' => array('target' => '_blank'))
          );
        }
      }
    }

    // For Terms of use taxonomy terms, if a URL was configured, make the name
    // clickable.
    if (
      !empty($node->content['lomch_rights_description']) &&
      $node->content['lomch_rights_description']['#formatter'] == 'taxonomy_term_reference_plain'
    ) {
      foreach ($node->content['lomch_rights_description'] as $key => $value) {
        if (is_numeric($key)) {
          $term = taxonomy_term_load(
            $node->content['lomch_rights_description']['#items'][$key]['tid']
          );

          // We use the 1st URL.
          if (!empty($term->archibald_license_url[LANGUAGE_NONE][0]['value'])) {
            $node->content['lomch_rights_description'][$key]['#markup'] = l(
              $node->content['lomch_rights_description'][$key]['#markup'],
              $term->archibald_license_url[LANGUAGE_NONE][0]['value'],
              array('attributes' => array('target' => '_blank'))
            );
          }
        }
      }
    }
  }
}

/**
 * Implements hook_load() for archibald_lomch_description.
 */
function archibald_lomch_description_load($nodes) {
  // Tests have shown this function can be called dozens of times for a single
  // node. Statically cache the database results.
  $data = &drupal_static(__FUNCTION__);

  foreach ($nodes as &$node) {
    if (!isset($data[$node->nid]['lom_id'])) {
      $data[$node->nid]['lom_id'] = db_select('archibald_lom_id', 'a')
                                      ->fields('a', array('lom_id'))
                                      ->condition('nid', $node->nid)
                                      ->execute()
                                      ->fetchField();
    }

    $node->lom_id = !empty($data[$node->nid]['lom_id']) ?
      $data[$node->nid]['lom_id'] :
      NULL;

    if (!isset($data[$node->nid]['lom_hash'])) {
      $data[$node->nid]['lom_hash'] = db_select('archibald_lom_hash', 'a')
                                        ->fields('a', array('published_hash', 'current_hash'))
                                        ->condition('nid', $node->nid)
                                        ->execute()
                                        ->fetchAssoc();
    }

    $node->published_hash = !empty($data[$node->nid]['lom_hash']['published_hash']) ?
      $data[$node->nid]['lom_hash']['published_hash'] :
      NULL;
    $node->current_hash = !empty($data[$node->nid]['lom_hash']['current_hash']) ?
      $data[$node->nid]['lom_hash']['current_hash'] :
      NULL;
  }
}

/**
 * Implements hook_update() for archibald_lomch_description.
 */
function archibald_lomch_description_update($node) {
  // Is the node being unpublished? If so, trigger the same logic as if a
  // specific revision was being unpublished.
  if (
    $node->status == NODE_NOT_PUBLISHED &&
    $node->original->status == NODE_PUBLISHED
  ) {
    archibald_revision_unpublish($node);
  }
}

/**
 * Implements hook_entity_update().
 */
function archibald_entity_update($entity, $entity_type, $entity_bundle = NULL) {
  // Did we just update an entity that is referenced by a currently
  // published archibald_lomch_description node? If so, we may need to update
  // the hash for a currently published description.
  $update_nodes = array();
  if (in_array($entity_type, array('taxonomy_term', 'archibald_vcard', 'archibald_partner'))) {
    module_load_include('inc', 'archibald', 'includes/archibald.entities');

    // Iterate over all fields that can reference the entity.
    foreach (archibald_archibald_lomch_description_fields() as $field_name => $field_options) {
      // Prepare an EntityFieldQuery.
      $query = new EntityFieldQuery();
      $query->entityCondition('entity_type', 'node')
            ->entityCondition('bundle', 'archibald_lomch_description')
            // We only care about nodes that have a published revision. The others
            // don't use any hash verification anyway (yet).
            ->propertyCondition('status', NODE_PUBLISHED)
            // We want to query revision specific data, because the latest
            // revision may not be published yet.
            ->age(FIELD_LOAD_REVISION);

      if (
        $entity_type == 'taxonomy_term' &&
        $field_options['type'] == 'taxonomy_term_reference'
      ) {
        $query->fieldCondition($field_name, 'tid', $entity->tid, '=');
      }
      elseif (
        $entity_type == 'archibald_vcard' &&
        $field_options['type'] == 'entityreference' &&
        !empty($field_options['settings']['target_type']) &&
        $field_options['settings']['target_type'] == 'archibald_vcard'
      ) {
        $query->fieldCondition($field_name, 'target_id', $entity->id, '=');
      }
      elseif (
        $entity_type == 'archibald_partner' &&
        $field_options['type'] == 'entityreference' &&
        !empty($field_options['settings']['target_type']) &&
        $field_options['settings']['target_type'] == 'archibald_partner'
      ) {
        $query->fieldCondition($field_name, 'target_id', $entity->id, '=');
      }
      else {
        continue;
      }

      // Execute the query.
      $result = $query->execute();

      if (!empty($result['node'])) {
        foreach ($result['node'] as $node) {
          // Overwrite duplicates. We only want to recompute once, after all.
          $update_nodes[$node->nid] = $node->vid;
        }
      }
    }
  }

  foreach ($update_nodes as $nid => $vid) {
    // Re-computing the hash right now is not feasible if we have a very
    // large amount of nodes. Instead, add all nodes to the queue,
    // flagging them as needing to be rehashed. Make sure we don't queue
    // the same node twice by clearing existing queues.
    archibald_clear_queue("node:{$nid}:rehash");
    archibald_add_to_queue(
      "node:{$nid}:rehash",
      '_archibald_recompute_hash',
      array($nid, $vid, 'current')
    );
  }
}

/**
 * Implements hook_delete() for archibald_lomch_description.
 */
function archibald_lomch_description_delete($node) {
  module_load_include('inc', 'archibald', 'includes/archibald.publish');

  // Queue all pending queues concerning this node. They will probably not work
  // anymore once it is removed.
  archibald_clear_queue("node:{$node->nid}:*");

  // Remove all data from the database.
  db_delete('archibald_lom_id')
    ->condition('nid', $node->nid)
    ->execute();

  db_delete('archibald_lom_hash')
    ->condition('nid', $node->nid)
    ->execute();

  // Trigger the deletion of this node.
  if (
    isset($node->lom_id) &&
    isset($node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id']) &&
    empty($node->only_delete_local)
  ) {
    if (!archibald_delete_description($node)) {
      drupal_set_message(t("Something went wrong when trying to delete %title from the catalog. It will automatically be retried later, during the next cron run.", array(
        '%title' => $node->title,
      )), 'warning');
    }
  }
}

/**
 * Implements hook_revisionapi().
 *
 * Legacy revisioning hook. There is no hook yet for the deletion of a
 * revision. We have to implement this one and check the $op variable.
 */
function archibald_revisionapi($op, $node) {
  // We can abort a publication if the node is not valid. This is a failsafe;
  // normally, all use cases should already be covered.
  if ($op == 'pre publish') {
    if (
      $node->type == 'archibald_lomch_description' &&
      empty($node->bypass_publishing)
    ) {
      module_load_include('inc', 'archibald', 'includes/archibald.publish');
      $result = archibald_validate_description($node);
      if (empty($result['valid'])) {
        return FALSE;
      }
    }
  }
  elseif ($op == 'post delete') {
    // Remove the publication hash from the database.
    db_delete('archibald_lom_hash')
      ->condition('nid', $node->nid)
      ->condition('vid', $node->vid)
      ->execute();
  }
}

/**
 * Implements hook_revision_publish().
 */
function archibald_revision_publish($node) {
  if ($node->type == 'archibald_lomch_description' && empty($node->bypass_publishing)) {
    module_load_include('inc', 'archibald', 'includes/archibald.publish');
    if (!archibald_publish_description($node)) {
      drupal_set_message(t("An error occurred when trying to publish the description on the API. It will automatically be re-tried in a few moments."), 'warning');
    }
  }
}

/**
 * Implements hook_revision_unpublish().
 */
function archibald_revision_unpublish($node) {
  if ($node->type == 'archibald_lomch_description') {
    // Was this the current revision? If so, it is no longer published, and must
    // also be removed from the national catalog.
    if (
      $node->is_current &&
      isset($node->lom_id) &&
      isset($node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id'])
    ) {
      module_load_include('inc', 'archibald', 'includes/archibald.publish');

      if (!archibald_unpublish_description($node)) {
        drupal_set_message(t("An error occurred when trying to unpublish the description on the API. It will automatically be re-tried in a few moments."), 'warning');
      }
    }

    // Make sure the node doesn't drop in "archive limbo".
    archibald_lomch_description_save_from_archive_limbo($node);
  }
}

/**
 * Implements hook_action_info().
 */
function archibald_action_info() {
  // This is a little trick to put the action implementations in their own file.
  // Because the result of this hook is not cached, it is called every time an
  // action is to be performed. This allows us to load our action definitions
  // only when they might be needed.
  module_load_include('inc', 'archibald', 'includes/archibald.actions');

  return array(
    'archibald_assign_to_user_action' => array(
      'type' => 'node',
      'label' => t("Assign to user (Archibald)"),
      'configurable' => TRUE,
      'behavior' => array('changes_property'),
      'triggers' => array('node_presave'),
    ),
    'archibald_assign_to_partner_action' => array(
      'type' => 'node',
      'label' => t("Assign to partner"),
      'configurable' => TRUE,
      'behavior' => array('changes_property'),
      'triggers' => array('node_presave'),
    ),
    'archibald_assign_to_catalog_action' => array(
      'type' => 'node',
      'label' => t("Assign to catalog(s)"),
      'configurable' => TRUE,
      'behavior' => array('changes_property'),
      'triggers' => array('node_presave'),
    ),
    'archibald_publish_action' => array(
      'type' => 'node',
      'label' => t("Publish description"),
      'configurable' => FALSE,
      // This is a hack we need to put in place to prevent VBO to trigger a save
      // at the end of our execution. It's not a valid behavior, but it prevents
      // a default one to be added.
      'behavior' => array('none'),
    ),
    'archibald_validate_action' => array(
      'type' => 'node',
      'label' => t("Validate description"),
      'configurable' => FALSE,
      // This is a hack we need to put in place to prevent VBO to trigger a save
      // at the end of our execution. It's not a valid behavior, but it prevents
      // a default one to be added.
      'behavior' => array('none'),
    ),
    'archibald_republish_action' => array(
      'type' => 'node',
      'label' => t("Republish description"),
      'configurable' => FALSE,
      // This is a hack we need to put in place to prevent VBO to trigger a save
      // at the end of our execution. It's not a valid behavior, but it prevents
      // a default one to be added.
      'behavior' => array('none'),
    ),
  );
}

/**
 * Implements hook_field_info().
 */
function archibald_field_info() {
  return array(
    'archibald_lomch_udi_identifier' => array(
      'label' => t("LOM-CH URL/DOI/ISBN Identifier"),
      'description' => t("LOM-CH identifier field type, allowing to define URL, DOI or ISBN links."),
      'instance_settings' => array(
        'show_title' => 1,
      ),
      'default_widget' => 'archibald_lomch_udi_identifier',
      'default_formatter' => 'archibald_lomch_udi_identifier',
    ),
    'archibald_lomch_ontology_vocabulary' => array(
      'label' => t("LOM-CH Ontology vocabulary"),
      'description' => t("LOM-CH vocabulary terms synchronized from the Ontology server."),
      'instance_settings' => array(
        'ontology_vocabulary_id' => '',
      ),
      'default_widget' => 'options_buttons',
      'default_formatter' => 'archibald_lomch_ontology_vocabulary',
    ),
    'archibald_lomch_filesize' => array(
      'label' => t("Filesize"),
      'description' => t("File size field"),
      'default_widget' => 'archibald_lomch_filesize',
      'default_formatter' => 'archibald_lomch_filesize',
    ),
  );
}

/**
 * Implements hook_field_info_alter().
 */
function archibald_field_info_alter(&$info) {
  // We add a new setting to all field types. This setting will allow fields to
  // be marked as "LOM-CH required" or "LOM-CH recommended". See
  // archibald_form_field_ui_field_edit_form_alter() for more information.
  foreach ($info as $field_type => $field_type_info) {
    $info[$field_type]['instance_settings'] += array(
      'archibald_lom_required' => FALSE,
      'archibald_lom_recommended' => FALSE,
    );
  }
}

/**
 * Implements hook_field_storage_details_alter().
 */
function archibald_field_storage_details_alter(&$details, &$field) {
  if (
    $field['type'] === 'taxonomy_term_reference' &&
    $field['settings']['options_list_callback'] == 'i18n_taxonomy_allowed_values'
  ) {
    module_load_include('inc', 'archibald', 'includes/archibald.entities');
    $fields_info = archibald_archibald_lomch_description_fields();

    // i18n_taxonomy uses this hook_field_storage_details_alter() hack to force
    // its own options callback. But we already have our own. If this is one of
    // our fields, put our own callback back again.
    if (!empty($fields_info[$field['field_name']]['settings']['options_list_callback'])) {
      $field['settings']['options_list_callback']= $fields_info[$field['field_name']]['settings']['options_list_callback'];
    }
  }
}

/**
 * Implements hook_field_instance_settings_form().
 *
 * @see includes/archibald.field.inc
 */
function archibald_field_instance_settings_form($field, $instance) {
  module_load_include('inc', 'archibald', 'includes/archibald.field');
  $function = "{$field['type']}_field_instance_settings_form";

  if (function_exists($function)) {
    return $function($field, $instance);
  }
}

/**
 * Implements hook_field_validate().
 *
 * @see includes/archibald.field.inc
 */
function archibald_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  module_load_include('inc', 'archibald', 'includes/archibald.field');
  $function = "{$field['type']}_field_validate";

  if (function_exists($function)) {
    $function($entity_type, $entity, $field, $instance, $langcode, $items, $errors);
  }
}

/**
 * Implements hook_field_is_empty().
 *
 * @see includes/archibald.field.inc
 */
function archibald_field_is_empty($item, $field) {
  module_load_include('inc', 'archibald', 'includes/archibald.field');
  $function = "{$field['type']}_field_is_empty";

  if (function_exists($function)) {
    return $function($item, $field);
  }
}

/**
 * Implements hook_options_list().
 *
 * @see includes/archibald.field.inc
 */
function archibald_options_list($field, $instance, $entity_type, $entity) {
  module_load_include('inc', 'archibald', 'includes/archibald.field');
  $function = "{$field['type']}_options_list";

  if (function_exists($function)) {
    return $function($field, $instance, $entity_type, $entity);
  }
}

/**
 * Implements hook_field_widget_info().
 */
function archibald_field_widget_info() {
  return array(
    'archibald_lomch_udi_identifier' => array(
      'label' => t("LOM-CH URL/DOI/ISBN identifier"),
      'field types' => array('archibald_lomch_udi_identifier'),
    ),
    'archibald_lomch_filesize' => array(
      'label' => t("Filesize"),
      'field types' => array('archibald_lomch_filesize'),
    ),
  );
}

/**
 * Implements hook_field_widget_info_alter().
 */
function archibald_field_widget_info_alter(&$info) {
  // Allow our custom archibald_lomch_ontology_vocabulary field to use the
  // options_select and options_buttons widgets.
  $info['options_select']['field types'][] = 'archibald_lomch_ontology_vocabulary';
  $info['options_buttons']['field types'][] = 'archibald_lomch_ontology_vocabulary';
}

/**
 * Implements hook_multiple_field_remove_button_field_widgets_alter().
 */
function archibald_multiple_field_remove_button_field_widgets_alter(&$widgets) {
  $widgets[] = 'archibald_lomch_udi_identifier';
}

/**
 * Implements hook_field_widget_form().
 *
 * @see includes/archibald.field.inc
 */
function archibald_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  module_load_include('inc', 'archibald', 'includes/archibald.field');
  $function = "{$instance['widget']['type']}_widget_form";

  if (function_exists($function)) {
    return $function($form, $form_state, $field, $instance, $langcode, $items, $delta, $element);
  }
  else {
    return $element;
  }
}

/**
 * Implements hook_field_widget_form_alter().
 *
 * We have 2 custom ways to alter a widget:
 * - based on the field type, we construct a callback named
 *   archibald_FIELD_TYPE_field_widget_form_alter().
 * - based on the field name, we construct a callback named
 *   archibald_FIELD_NAME_field_widget_form_alter().
 *
 * @see includes/archibald.field.inc
 */
function archibald_field_widget_form_alter(&$element, &$form_state, $context) {
  module_load_include('inc', 'archibald', 'includes/archibald.field');

  // WARNING: We use the field type here, NOT the widget type.
  $function = "archibald_{$context['field']['type']}_field_widget_form_alter";

  if (function_exists($function)) {
    $function($element, $form_state, $context);
  }

  // WARNING: We use the field name here, NOT the widget type.
  $function = "archibald_{$context['field']['field_name']}_field_widget_form_alter";

  if (function_exists($function)) {
    $function($element, $form_state, $context);
  }
}

/**
 * Implements hook_field_widget_error().
 *
 * @see archibald_field_validate()
 */
function archibald_field_widget_error($element, $error, $form, &$form_state) {
  switch ($error['error']) {
    case 'archibald_udi_url_invalid':
    case 'archibald_udi_isbn_invalid':
    case 'archibald_udi_doi_invalid':
    case 'archibald_filesize_invalid':
      form_error($element['value'], $error['message']);
      break;
  }
}

/**
 * Implements hook_field_formatter_info().
 */
function archibald_field_formatter_info() {
  return array(
    'archibald_lomch_udi_identifier' => array(
      'label' => t("LOM-CH URL/DOI/ISBN identifier formatter"),
      'field types' => array('archibald_lomch_udi_identifier'),
    ),
    'archibald_lomch_ontology_vocabulary' => array(
      'label' => t("LOM-CH Ontology vocabulary formatter"),
      'field types' => array('archibald_lomch_ontology_vocabulary'),
    ),
    'archibald_lomch_filesize' => array(
      'label' => t("Filesize"),
      'field types' => array('archibald_lomch_filesize'),
    ),
  );
}

/**
 * Implements hook_field_formatter_info_alter().
 */
function archibald_field_formatter_info_alter(&$info) {
  $info['list_default']['field types'][] = 'archibald_lomch_ontology_vocabulary';
}

/**
 * Implements hook_field_formatter_view().
 *
 * @see includes/archibald.field.inc
 */
function archibald_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  module_load_include('inc', 'archibald', 'includes/archibald.field');
  $function = "{$display['type']}_field_formatter_view";

  if (function_exists($function)) {
    return $function($entity_type, $entity, $field, $instance, $langcode, $items, $display);
  }
  else {
    return array();
  }
}

/**
 * Implements hook_views_api().
 */
function archibald_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'archibald') . '/includes',
  );
}

/**
 * Implements hook_form_FORM_ID_alter() for views_exposed_form().
 *
 * Alter the views exposed filter form to provide a better UX.
 */
function archibald_form_views_exposed_form_alter(&$form, $form_state) {
  module_load_include('inc', 'archibald', 'includes/archibald.views');
  $view = $form_state['view'];
  if ($view->name == 'archibald_lomch_descriptions') {
    archibald_lomch_descriptions_views_exposed_form_alter($form, $form_state);
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for taxonomy_overview_terms().
 *
 * Add information on whether the taxonomy term is "active" or not. This only
 * applies to certain vocabularies used in the description forms.
 */
function archibald_form_taxonomy_overview_terms_alter(&$form, $form_state, $vocabulary) {
  if (in_array($form['#vocabulary']->machine_name, array(
    'archibald_lomch_description_languages',
    'archibald_lomch_description_licenses',
  ))) {
    // Taxonomy term items have a key prefixed with "tid:".
    foreach ($form as $key => &$element) {
      if (preg_match('/^tid:\d+/', $key)) {
        // The terms in the default form don't have any field information. We
        // have to load them fully to get access to it.
        $term = taxonomy_term_load($element['#term']['tid']);

        // Add some information to the "view" key based on the
        // archibald_term_status field.
        $element['view']['#title'] .= ' (' . t("Archibald status: !status", array(
            '!status' => !empty($term->archibald_term_status[LANGUAGE_NONE][0]['value']) ? t("active") : t("inactive")
        )) . ')';
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for views_form_admin_views_node_system_1().
 */
function archibald_form_views_form_admin_views_node_system_1_alter(&$form, $form_state) {
  $form['select']['submit']['#validate'][] = 'archibald_form_node_admin_content_alter_validate';
}

/**
 * Implements hook_form_FORM_ID_alter() for node_admin_content().
 *
 * Block the publishing of archibald_lomch_description content if it is not
 * valid.
 */
function archibald_form_node_admin_content_alter(&$form, $form_state) {
  $form['admin']['options']['submit']['#validate'][] = 'archibald_form_node_admin_content_alter_validate';
}

/**
 * Validation callback for archibald_form_node_admin_content_alter().
 */
function archibald_form_node_admin_content_alter_validate($form, $form_state) {
  // Make sure we are not "publishing" descriptions that are not valid. This
  // is confusing to users, as they would be marked as published in Drupal,
  // but in reality would have been rejected by the REST API.
  if ($form_state['values']['operation'] == 'publish') {
    $nids = array_filter($form_state['values']['nodes']);
  }
  elseif ($form_state['values']['operation'] == 'action::node_publish_action') {
    $nids = $form_state['values']['views_bulk_operations'];
  }
  else {
    // Not a publishing action. We don't care.
    return;
  }

  // Instead of loading all nodes and checking their type, only load the nids
  // that belong to archibald_lomch_description nodes. This will save a lot of
  // database queries.
  $description_nids = array_keys(db_select('node', 'n')
                        ->fields('n', array('nid', 'vid'))
                        ->condition('type', 'archibald_lomch_description')
                        ->condition('nid', $nids, 'IN')
                        ->execute()
                        ->fetchAllKeyed());

  if (!empty($description_nids)) {
    module_load_include('inc', 'archibald', 'includes/archibald.publish');
    foreach ($description_nids as $nid) {
      $node = node_load($nid);

      // Perform the validation.
      $result = archibald_validate_description($node);

      // Is it valid? If not, block the form submission.
      if (empty($result['valid'])) {
        form_set_error("nodes[{$nid}]", t("Couldn't publish %title, as it is not valid. Please check <a href='!link'>this page</a> for more information.", array(
          '%title' => $node->title,
          '!link' => url("node/{$node->nid}/validate"),
        )));
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for node_type_form().
 *
 * Lock the Revisioning module options for the archibald_lomch_description
 * content type. Archibald requires a very specific mix of options to function
 * properly.
 */
function archibald_form_node_type_form_alter(&$form, $form_state) {
  if ($form['#node_type']->type == 'archibald_lomch_description') {
    // Disable the revision options for the node type. We always want to create
    // new revisions, and always want to keep the latest in draft/pending.
    $form['workflow']['node_options']['revision']['#disabled'] = TRUE;
    $form['workflow']['node_options']['revision']['#value'] = 'revision';
    $form['workflow']['node_options']['revision_moderation']['#disabled'] = TRUE;
    $form['workflow']['node_options']['revision_moderation']['#value'] = 'revision_moderation';

    // Disable the new revisions options. We only allow '0', meaning create
    // a new revision in draft on edits, and keeping only one in draft at any
    // given time.
    $form['workflow']['revisioning']['new_revisions']['#disabled'] = TRUE;
    $form['workflow']['revisioning']['new_revisions']['#value'] = 0;
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for archibald_lomch_description_node_form().
 *
 * - Do not let the user choose the way the current revision edition should be
 *   treated. Archibald needs the revision to be kept in moderation
 *   (draft/pending), and to have only one of those at any given time.
 * - Hide the publishing options. They are to be managed through the Revisioning
 *   UI, and only serve to confuse our users.
 * - Hide the author information. It is to be managed through our admin lists,
 *   and only serves to confuse our users.
 * . Change the label of the core "Language" selector field.
 * - Add our JS logic.
 * - Add our CSS.
 * - Add description to the entity reference fields for VCards.
 * - Add required marker to certain fieldsets and containers.
 * - Translate Language of the resource vocabulary items using locale().
 *
 * @see archibald_generate_vcard_help_text()
 */
function archibald_form_archibald_lomch_description_node_form_alter(&$form, $form_state) {
  global $user;

  $form['revision_information']['#access'] = FALSE;
  $form['options']['#access'] = FALSE;
  $form['author']['#access'] = FALSE;
  $form['language']['#title'] = t("Description language", array(), array('context' => 'archibald:node_form'));

  $path = drupal_get_path('module', 'archibald');
  $form['#attached']['js'][] = "$path/js/archibald.lomch_description_node_form.js";
  $form['#attached']['js'][] = array(
    'data' => array(
      'archibald' => array(
        'activateVTabFooterLinks' => variable_get('archibald_activate_footer_links', 0),
        'activateCollapsibleVTabs' => variable_get('archibald_activate_collapsible_vtabs', 0),
        'activateStickyVTabs' => variable_get('archibald_activate_sticky_vtabs', 0),
      ),
    ),
    'type' => 'setting',
  );
  if (variable_get('archibald_activate_sticky_vtabs', 0)) {
    $form['#attached']['js'][] = "$path/js/jquery.sticky.js";
  }
  $form['#attached']['css'][] = "$path/css/archibald.lomch_description_node_form.css";

  // We have special styles for certain admin themes, to make the form look a
  // bit better. Make sure the admin theme is active, that it is active for
  // node forms, and that the user has access to it.
  archibald_add_admin_theme_styles(TRUE);

  // Alter the lomch_lifecycle_author, lomch_lifecycle_editor and
  // lomch_lifecycle_publisher form elements.
  foreach (array(
    'lomch_lifecycle_author',
    'lomch_lifecycle_editor',
    'lomch_lifecycle_publisher',
  ) as $key) {
    // We want to add a description with more information for the users. If we
    // were to use archibald_field_widget_form_alter() in this particular case,
    // the description would be added for each widget (being a multi-valued
    // field), but we want to add it to the "wrapper" only.
    $form[$key][LANGUAGE_NONE]['#description'] = archibald_generate_vcard_help_text($user);
  }

  // In Drupal forms, fields that are rendered as containers (like fieldsets)
  // don't use the same logic get a required marker, which means that, for our
  // custom archibald_lom_required and archibald_lom_recommended settings, they
  // don't get a required marker (their children do). But in our form, it
  // actually makes sense to add them to the parent, having multiple levels of
  // "required-ness". Check all fields that are susceptible to fit that use
  // case, and load their instance settings. If one of them is required in any
  // way, add a marker to the field title, as well as to the child items.
  module_load_include('inc', 'archibald', 'includes/archibald.entities');
  foreach(archibald_archibald_lomch_description_fields() as $field_name => $field_info) {
    if (
      !empty($form[$field_name]) &&
      ($field_info['cardinality'] > 1 || $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED) &&
      !in_array($field_info['type'], array('taxonomy_term_reference', 'archibald_lomch_ontology_vocabulary'))
    ) {
      $instance = field_info_instance('node', $field_name, 'archibald_lomch_description');
      if ($instance) {
        // Check the required settings. If it is "required" (like, the default
        // Drupal required), we don't need to do anything.
        if (
          !empty($instance['settings']['archibald_lom_required']) ||
          !empty($instance['settings']['archibald_lom_recommended'])
        ) {
          $vars = array(
            'element' => array(
              '#required_type' => !empty($instance['settings']['archibald_lom_required']) ?
                'archibald_lom_required' :
                'archibald_lom_recommended',
            ),
          );
          $marker = ' ' . theme_archibald_form_required_marker($vars);

          // Fetch the child elements, which are the actual wrappers.
          foreach ($form[$field_name] as $key => &$element) {
            if (!preg_match('/^#/', $key)) {
              $element['#title'] .= $marker;
            }
          }
        }
      }
    }
  }

  // The vocabulary archibald_lomch_description_languages contains many language
  // names in English, which were generated on the fly using Drupal's built-in
  // ISO language library. Because Drupal already has translations for most of
  // these language names, it's not worth it to make the vocabulary items
  // translatable. Instead, we "simply" loop over the options, and localize them
  // if necessary.
  if (function_exists('locale')) {
    $form['lomch_resource_language'][LANGUAGE_NONE]['#options'] = array_map(function($item) {
      return locale($item);
    }, $form['lomch_resource_language'][LANGUAGE_NONE]['#options']);
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for revisioning_publish_confirm().
 */
function archibald_form_revisioning_publish_confirm_alter(&$form, $form_state) {
  $node = $form_state['build_info']['args'][0];
  if ($node->type == 'archibald_lomch_description') {
    $form['#validate'][] = 'archibald_form_revisioning_publish_confirm_alter_validate';
  }
}

/**
 * Validation callback for revisioning_publish_confirm().
 */
function archibald_form_revisioning_publish_confirm_alter_validate($form, $form_state) {
  module_load_include('inc', 'archibald', 'includes/archibald.publish');

  // Don't allow a revision to be published if it is not valid.
  $node = $form_state['build_info']['args'][0];
  $result = archibald_validate_description($node);

  if (empty($result['valid'])) {
    form_set_error('confirm', t("Couldn't publish %title, as it is not valid. Please check <a href='!link'>this page</a> for more information.", array(
      '%title' => $node->title,
      '!link' => url("node/{$node->nid}/validate"),
    )));
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for revisioning_ux_publish().
 */
function archibald_form_revisioning_ux_publish_alter(&$form, $form_state) {
  // We simply proxy to archibald_form_revisioning_publish_confirm_alter().
  archibald_form_revisioning_publish_confirm_alter($form, $form_state);
}

/**
 * Implements hook_form_FORM_ID_alter() for simpletest_test_form().
 */
function archibald_form_simpletest_test_form_alter(&$form, $form_state) {
  // If simpletest_turbo exists, add the ability to clean up the cached tables
  // via the simpletest "Clean environment" button.
  if (module_exists('simpletest_turbo') && function_exists('simpletest_turbo_end')) {
    $form['clean']['op']['#submit'][] = 'simpletest_turbo_end';
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for field_ui_field_edit_form().
 *
 * For fields that are part of the archibald_lomch_description content type,
 * fields can be marked as required, but also as "LOM required" or
 * "LOM recommended". Add these settings in an intuitive format, by extending
 * the field_ui "required" checkbox.
 */
function archibald_form_field_ui_field_edit_form_alter(&$form, $form_state) {
  $instance = $form['#instance'];

  // Are we in context of a field that is part of the
  // archibald_lomch_description content type?
  if (!empty($instance['bundle']) && in_array($instance['bundle'], array('archibald_lomch_description', 'lomch_relations'))) {
    // Enhance the "required" checkbox by turning it into a radios list. Each
    // option will reflect the "required" level. This is just for UX. The actual
    // values are still separate, so field_ui can understand the form submission
    // and store the values. We simply hide the relevant fields, and put the
    // values in the correct places with our own submit handler,
    // archibald_form_field_ui_field_edit_form_alter_submit().
    $default_value = !empty($instance['required']) ? 'required' : 0;
    if (!$default_value) {
      $default_value = !empty($instance['settings']['archibald_lom_required']) ? 'archibald_lom_required' : 0;
    }
    if (!$default_value) {
      $default_value = !empty($instance['settings']['archibald_lom_recommended']) ? 'archibald_lom_recommended' : 0;
    }

    $form['instance']['required']['#type'] = 'radios';
    $form['instance']['required']['#options'] = array(
      0 => t("Not required"),
      'required' => $form['instance']['required']['#title'],
      'archibald_lom_required' => t("LOM-CH required field"),
      'archibald_lom_recommended' => t("LOM-CH recommended field"),
    );
    // We have to cast the default value to a string. If it is 0, it won't check
    // the "Not required" option by default.
    $form['instance']['required']['#default_value'] = (string) $default_value;
    $form['instance']['required']['#description'] = t("Marking this field as required for LOM-CH will not block a form submission if it is not filled, but it will block the publishing of a description later on.");

    // Add hidden inputs for our custom settings. We will proxy the user selection
    // in our custom submit handler later.
    $form['instance']['settings']['archibald_lom_required'] = array(
      '#type' => 'hidden',
      '#value' => $default_value == 'archibald_lom_required',
    );
    $form['instance']['settings']['archibald_lom_recommended'] = array(
      '#type' => 'hidden',
      '#value' => $default_value == 'archibald_lom_recommended',
    );

    // Add our submit handler. It has to be called first.
    $form['#submit'] = array_merge(array('archibald_form_field_ui_field_edit_form_alter_submit'), $form['#submit']);
  }
}

/**
 * Submission callback for field_ui_field_edit_form().
 *
 * Proxy the required field values to their correct, respective keys, as
 * expected by the field_ui module.
 */
function archibald_form_field_ui_field_edit_form_alter_submit($form, &$form_state) {
  // Handle the required states. Make sure the required field gets set back to
  // normal, and the selection gets correctly proxied to our hidden fields.
  $required_value = $form_state['values']['instance']['required'];
  $form_state['values']['instance']['required'] = $required_value == 'required';
  $form_state['values']['instance']['settings']['archibald_lom_required'] = $required_value == 'archibald_lom_required';
  $form_state['values']['instance']['settings']['archibald_lom_recommended'] = $required_value == 'archibald_lom_recommended';
}

/**
 * Implements hook_preprocess_form_element_label().
 */
function archibald_preprocess_form_element_label(&$vars) {
  $element = &$vars['element'];
  if (!empty($element['#bundle']) && in_array($element['#bundle'], array('archibald_lomch_description', 'lomch_relations'))) {
    $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);

    if ($instance['settings']['archibald_lom_required'] || $instance['settings']['archibald_lom_recommended']) {
      // Add the required star to the form label.
      $element['#required'] = TRUE;
      $element['#required_type'] = $instance['settings']['archibald_lom_required'] ? 'archibald_lom_required' : 'archibald_lom_recommended';
    }
    elseif ($element['#required']) {
      $element['#required_type'] = 'required';
    }
  }
}

/**
 * Implements hook_archibald_publish_description().
 */
function archibald_archibald_publish_description($op, $data, $node) {
  if ($op == 'pre publish') {
    // Check if we need to upload our images. If so, alter the payload data
    // before its sent. Upload all images (preview image, VCard logos) and
    // replace the data in the JSON representation.
    if (variable_get('archibald_host_images_on_api', FALSE)) {
      // Try authenticating. We abort if it doesn't work.
      try {
        // Initialize a new client.
        $client = archibald_get_client_based_on_config(
          $node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id']
        );
      }
      catch(Exception $e) {
        watchdog('archibald:publish', "Could not authenticate. Error: %e", array(
          '%e' => $e->getMessage(),
        ), WATCHDOG_ERROR);
        return FALSE;
      }

      // Do we have a preview image?
      if (!empty($node->lomch_preview_image)) {
        $file = file_load($node->lomch_preview_image[LANGUAGE_NONE][0]['fid']);
        if ($file) {
          $filepath = drupal_realpath($file->uri);

          if (file_exists($filepath) && is_readable($filepath)) {
            $result = $client->uploadFile($filepath);
            $data->technical->previewImage = new stdClass();
            $data->technical->previewImage->image = $result['fileUrl'];
          }
        }
      }

      // Check VCards, both in lifeCycle and metaMetadata.
      foreach (array('lifeCycle', 'metaMetadata') as $group) {
        if (!empty($data->{$group}) && !empty($data->{$group}->contribute)) {
          foreach ($data->{$group}->contribute as $i => $contribute) {
            if (!empty($contribute->entity)) {
              foreach ($contribute->entity as $j => $entity) {
                // Parse the VCard.
                $parser = new JeroenDesloovere\VCard\VCardParser($entity);
                $card = $parser->getCardAtIndex(0);

                // Is the logo a local file?
                if (
                  !empty($card->logo) &&
                  file_exists($card->logo) &&
                  is_readable($card->logo)
                ) {
                  try {
                    // Upload the file.
                    $result = $client->uploadFile($card->logo);

                    // VCard files have a 73 char line limit. This might not
                    // work for certain VCards!! Wait for a VCard::fromObject()
                    // method to exist, so we can re-export. If it is not
                    // available, fallback to the old way.
                    if (method_exists('JeroenDesloovere\VCard\VCard', 'fromObject')) {
                      $card->logo = $result['fileUrl'];
                      $vcard = JeroenDesloovere\VCard\VCard::fromObject($card, false);
                      $data->{$group}->contribute[$i]->entity[$j] = $vcard->buildVCard();
                    }
                    else {
                      $data->{$group}->contribute[$i]->entity[$j] = str_replace(
                        $card->logo,
                        $result['fileUrl'],
                        $data->{$group}->contribute[$i]->entity[$j]
                      );
                    }
                  }
                  catch(Exception $e) {
                    watchdog('archibald:publish', "Could not upload file %file. Error: %e", array(
                      '%file' => $card->logo,
                      '%e' => $e->getMessage(),
                    ), WATCHDOG_ERROR);
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

/**
 * Implements hook_preprocess_views_view().
 */
function archibald_preprocess_views_view(&$vars) {
  $view = &$vars['view'];

  if ($view->name == 'archibald_vcards' && $view->current_display == 'references_dialog_search') {
    archibald_add_admin_theme_styles();
  }
}

/**
 * Implements hook_preprocess_references_dialog_links().
 */
function archibald_preprocess_references_dialog_links(&$vars) {
  if (!empty($vars['links']) && function_exists('locale')) {
    // References Dialog uses Views to provide a search UI for entity reference.
    // For performance reasons, these views are cached. However, the button
    // label uses the view label. But because it's cached, the label will be in
    // the language of the session during which the cache was cleared. We
    // correct it here for our VCard entity references, by preprocessing the
    // rendered buttons.
    foreach ($vars['links'] as &$link) {
      if (preg_match('/^references-dialog\/search\/archibald_vcards/', $link['href'])) {
        $link['title'] = t("Search");
      }
    }
  }
}

/**
 * Implements hook_references_dialog_entity_admin_paths().
 */
function archibald_references_dialog_entity_admin_paths() {
  return array(
    'archibald_vcard' => array(
      'add' => 'admin/archibald/vcard/add',
      'edit' => 'admin/archibald/vcard/manage/[entity_id]',
    ),
  );
}

/**
 * Implements hook_ENTITY_view() for archibald_partner.
 */
function archibald_archibald_partner_view($entity, $view_mode, $langcode) {
  // Add a custom theme hook, instead of the default "entity".
  $entity->content['#theme'] = 'archibald_partner_' . $view_mode;
}

/**
 * Access callback for the node edit/view tabs.
 *
 * Revisioning has an odd behavior where it will prevent duplicate tabs from
 * showing up. So, if viewing the latest revision, and the Revisioning "load op"
 * setting is set to REVISIONING_LOAD_LATEST, the primary "View" and "Edit" tabs
 * will not show up, because they are already present in the secondary tabs. The
 * same thing happens when viewing the current revision, and the setting is
 * REVISIONING_LOAD_CURRENT.
 *
 * We hijack the access callback, and make sure these tabs are always visible
 * for our archibald_lomch_description content type, regardless of the setting.
 * This is less confusing for our users.
 *
 * @see _revisioning_view_edit_access_callback()
 *
 * @param string $op
 *     Must be one of 'view' or 'edit'.
 * @param object $node
 *     The node object.
 *
 * @return bool
 *     False if access to the desired revision is denied.
 */
function archibald_revisioning_view_edit_access_callback($op, $node) {
  // We only do this for our own node type. For the others, we let Revisioning
  // do its thing.
  if ($node->type == 'archibald_lomch_description') {
    if (is_numeric(arg(3))) {
      return TRUE;
    }
  }

  return _revisioning_view_edit_access_callback($op, $node);
}

/**
 * Access callback for the JSON export page.
 *
 * @param object $node
 *    The node that is being viewed.
 *
 * @return bool
 *    True if access was granted, false otherwise.
 */
function archibald_json_access($node) {
  if ($node->type != 'archibald_lomch_description') {
    return FALSE;
  }

  return node_access('view', $node);
}

/**
 * Access callback for the default Views.
 *
 * @param string $display
 *    The view display.
 *
 * @return bool
 *    True if access was granted, false otherwise.
 */
function archibald_views_access($display) {
  // If we are dealing with a content administrator, we always grant access.
  if (user_access('administer nodes')) {
    return TRUE;
  }

  // We are not dealing with an administrator. We need to check a specific set
  // of permissions before we can grant access.
  switch ($display) {
    case 'proposals':
      // Proposals require users to see other people's unpublished content. This
      // is only possible for administrators. So, if we got this far, the user
      // has no access.
      return FALSE;

    case 'pending':
      // Pending requires a user to at least see one's own unpublished content.
      // Furthermore, it must allow a user to edit one's own content, or
      // anyone's content. Normally, editing anyone's content makes no sense
      // here, as the other people's content will be unpublished at this stage,
      // so invisible (unless the user is an admin, but that was already covered
      // above). However, in order to make things more intuitive, we allow
      // the "edit any content" permission as well.
      return
        user_access('view own unpublished content') &&
        (
          user_access('edit own archibald_lomch_description content') ||
          user_access('edit any archibald_lomch_description content')
        );

    case 'published':
    case 'republish':
      // Published and To republish requires a user to be able to (un)publish
      // revisions, be it of her own content, or someone else's. Note that the
      // "publish revisions" and "unpublish current revision" are not sufficient
      // on their own, but because the permissions page clearly explains that
      // these 2 permissions require other permissions to work correctly, we can
      // assume this is the case. In the worst case, the user will simply see a
      // list of content to which she probably was meant to have access anyway,
      // but won't be able to perform any actions.
      // Note that we don't check against actions_permissions' permissions, as
      // the module is not required for Archibald to work. This means a user
      // could get access to actions she normally should not have access to.
      return
        user_access('publish revisions') ||
        user_access('unpublish current revision') ||
        user_access('publish revisions of own archibald_lomch_description content') ||
        user_access('publish revisions of any archibald_lomch_description content');

    default:
      return FALSE;
  }
}

/**
 * Access callback for our custom archibald_vcard entity.
 */
function archibald_vcard_access($op, $vcard = NULL, $account = NULL) {
  if (empty($account)) {
    global $user;
    $account = $user;
  }

  if (user_access('administer archibald_vcards', $account)) {
    return TRUE;
  }
  elseif ($op == 'view') {
    // Users require at least 'view archibald_vcards'. If they don't have it,
    // then we never grant access.
    if (!user_access('view archibald_vcards', $account)) {
      return FALSE;
    }
    else {
      // Check if any module wants to restrict access.
      return !in_array(FALSE, module_invoke_all('archibald_vcard_access', $op, $partner, $account));
    }
  }
  else {
    return FALSE;
  }
}

/**
 * Access callback for our custom archibald_partner entity.
 */
function archibald_partner_access($op, $partner = NULL, $account = NULL) {
  if (empty($account)) {
    global $user;
    $account = $user;
  }

  if (user_access('administer archibald_partners', $account)) {
    return TRUE;
  }
  elseif ($op == 'view') {
    // Users require at least 'view archibald_partners'. If they don't have it,
    // then we never grant access.
    if (!user_access('view archibald_partners', $account)) {
      return FALSE;
    }
    else {
      // Check if any module wants to restrict access.
      return !in_array(FALSE, module_invoke_all('archibald_partner_access', $op, $partner, $account));
    }
  }
  else {
    return FALSE;
  }
}

/**
 * Libraries API version callback.
 *
 * Libraries API requires a version for each library it loads. This function
 * parses the CHANGELOG.md file and extracts the version information.
 *
 * @param array $library_info
 *    The library information, passed by Libraries API.
 *
 * @return string
 *    The parsed version, or 'n/a' if none was found.
 */
function archibald_get_library_version($library_info) {
  $changelog_file = $library_info['library path'] . '/CHANGELOG.md';

  if (!file_exists($changelog_file)) {
    $changelog_file = $library_info['library path'] . '/CHANGELOG.rst';
  }

  if (file_exists($changelog_file)) {
    $changelog = file_get_contents($changelog_file);
    $match;

    if (preg_match('/^#*\**\s*([0-9\.]+)\s+/m', $changelog, $match)) {
      return $match[1];
    }
  }

  return 'n/a';
}

/**
 * Form callback for archibald_vcard entity.
 *
 * As we use a fieldable entity, and have no special properties, we simply
 * return a submit button. The 'name' property will be derived from the fields
 * and is hidden.
 */
function archibald_vcard_form($form, &$form_state, $vcard = NULL) {
  field_attach_form('archibald_vcard', $vcard, $form, $form_state);

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => isset($vcard->id) ? t("Update VCard") : t("Save VCard"),
    '#weight' => 50,
  );

  return $form;
}

/**
 * Validation handler for archibald_vcard_form().
 */
function archibald_vcard_form_validate($form, $form_state) {
  // We need either a first name and last name, or an organization.
  if (
    empty($form_state['values']['vcard_first_name'][LANGUAGE_NONE][0]['value']) &&
    empty($form_state['values']['vcard_last_name'][LANGUAGE_NONE][0]['value']) &&
    empty($form_state['values']['vcard_organization'][LANGUAGE_NONE][0]['value'])
  ) {
    form_set_error('vcard_first_name', t("You must at least provide an organization name, or a first and last name."));
    form_set_error('vcard_last_name');
    form_set_error('vcard_organization');
  }
  elseif (
    !empty($form_state['values']['vcard_first_name'][LANGUAGE_NONE][0]['value']) &&
    empty($form_state['values']['vcard_last_name'][LANGUAGE_NONE][0]['value'])
  ) {
    form_set_error('vcard_last_name', t("If you provide a first name, you must also provide a last name."));
  }
  elseif (
    empty($form_state['values']['vcard_first_name'][LANGUAGE_NONE][0]['value']) &&
    !empty($form_state['values']['vcard_last_name'][LANGUAGE_NONE][0]['value'])
  ) {
    form_set_error('vcard_first_name', t("If you provide a last name, you must also provide a first name."));
  }
}

/**
 * Submit handler for archibald_vcard_form().
 */
function archibald_vcard_form_submit($form, &$form_state) {
  $vcard = entity_ui_form_submit_build_entity($form, $form_state);

  // Prepare the name based on the field information. Either we have a last
  // name, in which case we use the format "lastname firstname (organization)",
  // or not, in case we only use the organization name.
  if (!empty($vcard->vcard_last_name[LANGUAGE_NONE][0]['value'])) {
    $name = "{$vcard->vcard_last_name[LANGUAGE_NONE][0]['value']} {$vcard->vcard_first_name[LANGUAGE_NONE][0]['value']}";

    if (!empty($vcard->vcard_organization[LANGUAGE_NONE][0]['value'])) {
      $name .= " ({$vcard->vcard_organization[LANGUAGE_NONE][0]['value']})";
    }
  }
  else {
    $name = $vcard->vcard_organization[LANGUAGE_NONE][0]['value'];
  }

  $vcard->name = $name;
  $vcard->changed = REQUEST_TIME;
  $vcard->save();

  drupal_set_message(t('The VCard %name has been saved.', array('%name' => $vcard->name)));
  $form_state['redirect'] = 'admin/archibald/vcard';
}

/**
 * Form callback for archibald_partner entity.
 */
function archibald_partner_form($form, &$form_state, $partner = NULL) {
  // Do we have a private file system? If not, we cannot create partners.
  if (
    !($wrapper = file_stream_wrapper_get_instance_by_uri('private://')) ||
    !($key = archibald_get_encryption_key())
  ) {
    // Display a different message if the user has access to configure a
    // private file system.
    if (user_access('administer site configuration')) {
      if (!$wrapper) {
        $message = t("You must configure a private file system in order to create Partners. You will be requested to upload a private key, which should never be stored in a web-accessible directory. You can configure the private file system under !link.", array(
          '!link' => l('admin/config/media/file-system', 'admin/config/media/file-system'),
        ));
      }
      else {
        $message = t("Archibald needs an encryption key to securely store partner passphrases. You can find more information under !link.", array(
          '!link' => l('admin/reports/status', 'admin/reports/status'),
        ));
      }
    }
    else {
      $message = t("You cannot create Partners at the moment, because the site is not correctly configured. Please contact the site administrator.");
    }

    drupal_set_message($message, 'error');

    return array();
  }

  field_attach_form('archibald_partner', $partner, $form, $form_state);

  // Re-order the fields a bit.
  $form['partner_display_name']['#weight'] = -60;
  $form['partner_username']['#weight'] = -50;
  $form['partner_privatekey']['#weight'] = -40;

  // The passphrase is a password type field, so we handle it ourselves. It is
  // not a field, but a property.
  $form['passphrase'] = array(
    '#type' => 'password',
    '#title' => t("Passphrase"),
    '#description' => t("The passphrase for the private key, if any."),
    '#weight' => -30,
  );

  if (isset($partner->id)) {
    $form['passphrase']['#description'] .= ' ' . t("If you want to change the password, enter a new one. Otherwise, you can leave this field blank to use the password you provided earlier.");
  }

  // For faster lookups, we made the "is_default" flag a property as well.
  $form['is_default'] = array(
    '#type' => 'checkbox',
    '#title' => t("Make this partner the default"),
    '#description' => t("Archibald requires a default partner for loading certain information, like Ontology data. Marking this partner as default means it will be used to fetch this information. Marking this partner as default will mark all others as non-default."),
    '#default_value' => !empty($partner->is_default) ? $partner->is_default : 0,
    '#weight' => 40,
  );

  // Update partner data on the API.
  $form['update_remotely'] = array(
    '#type' => 'checkbox',
    '#title' => t("Update the data of this partner on the API"),
    '#description' => t("When saving this partner, its information can be updated on the API as well. Use this to centrally manage your partner's information, like logo or website URL."),
    '#default_value' => 1,
    '#weight' => 50,
  );

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => isset($partner->id) ? t("Update Partner") : t("Save Partner"),
    '#weight' => 50,
  );

  $form['test_connection'] = array(
    '#type' => 'button',
    '#value' => t("Test connection"),
    '#name' => 'test_connection',
    '#weight' => 60,
  );

  $form['fetch_remote'] = array(
    '#type' => 'button',
    '#value' => t("Load data from the API"),
    '#name' => 'fetch_remote',
    '#weight' => 60,
  );

  return $form;
}

/**
 * Validate callback for archibald_partner_form().
 */
function archibald_partner_form_validate($form, &$form_state) {
  // If the "Test connection" button was clicked, we try to connect to the
  // API and see if the provided values are correct. If the "Load data from the
  // API" button was clicked, we go one step further, and pre-fill all fields
  // with data from the API.
  if (
    $form_state['triggering_element']['#name'] === 'test_connection' ||
    $form_state['triggering_element']['#name'] === 'fetch_remote'
  ) {
    $test_connection = TRUE;

    // The username is required.
    if (empty($form_state['values']['partner_username'][LANGUAGE_NONE][0]['value'])) {
      form_set_error('partner_username');
      $test_connection = FALSE;
    }

    // The private key is required as well.
    if (empty($form_state['values']['partner_privatekey'][LANGUAGE_NONE][0]['fid'])) {
      form_set_error('partner_privatekey');
      $test_connection = FALSE;
    }

    // Can we test the connection (no form errors)?
    if ($test_connection) {
      // Fetch the data.
      $partner = $form['#entity'];
      $api_version = variable_get('archibald_api_version', NULL);
      $api_url = variable_get('archibald_api_url', NULL);
      $username = $form_state['values']['partner_username'][LANGUAGE_NONE][0]['value'];
      $key_fid = $form_state['values']['partner_privatekey'][LANGUAGE_NONE][0]['fid'];
      $key_passphrase = $form_state['values']['passphrase'];

      if (empty($key_passphrase) && !empty($partner->passphrase) && !empty($partner->iv)) {
        $key_passphrase = archibald_decrypt($partner->passphrase, $partner->iv);
      }

      try {
        $client = archibald_get_client(
          $api_version,
          $api_url,
          $username,
          $key_fid,
          $key_passphrase
        );

        $client->authenticate();

        drupal_set_message(t("Authentication was successful."));
      }
      catch (Exception $e) {
        drupal_set_message(t("The connection failed. Error message: @message", array(
          '@message' => $e->getMessage(),
        )), 'error');
      }

      // Do we need to load data?
      if ($form_state['triggering_element']['#name'] === 'fetch_remote') {
        $data = $client->loadPartner(
          $username
        );
        foreach (array(
          'company' => 'partner_organization',
          'name' => 'partner_display_name',
          'country' => 'partner_country',
          'zip' => 'partner_zip',
          'city' => 'partner_city',
          'address' => 'partner_address',
          'address2' => 'partner_add_address',
          'address2' => 'partner_add_address',
          'website' => 'partner_url',
        ) as $key => $field_name) {
          $form_state['input'][$field_name][LANGUAGE_NONE][0]['value'] = !empty($data[$key]) ?
            $data[$key] :
            '';
        }

        // The email field has a different key.
        $form_state['input']['partner_email'][LANGUAGE_NONE][0]['email'] = !empty($data['email']) ?
          $data['email'] :
          '';

        // If there's a logo, try to download it and store it locally. Suppress
        // warnings for files that could not be found.
        if (!empty($data['logo'])) {
          $logo_success = TRUE;
          $file_data = @file_get_contents($data['logo']);
          if ($file_data) {
            $filename = @end(explode('/', $data['logo']));
            // Make sure there were no GET params after the file name.
            $filename = preg_replace('/\?.+$/', '', $filename);

            // Store it locally.
            if ($file = file_save_data($file_data, file_default_scheme() . "://{$filename}")) {
              $form_state['input']['partner_logo'][LANGUAGE_NONE][0]['fid'] = $file->fid;
              file_usage_add($file, 'archibald', 'archibald_partner', !empty($partner->id) ? $partner->id : 0);
            }
            else {
              $logo_success = FALSE;
            }
          }
          else {
            $logo_success = FALSE;
          }

          if (!$logo_success) {
            drupal_set_message(t("Could not download logo from the API. The logo field was not updated."), 'warning');
          }
        }

        drupal_set_message(t("Pre-filled fields with partner data from the API. The data has not been saved yet. Save the partner to store the new values."));
      }
    }
    else {
      drupal_set_message(t("Could not test connection. Please provide all necessary connection information."), 'error');
    }
  }
}

/**
 * Submit handler for archibald_partner_form().
 */
function archibald_partner_form_submit($form, &$form_state) {
  $already_encrypted = FALSE;
  $partner = entity_ui_form_submit_build_entity($form, $form_state);

  // The passphrase may have been overridden by the empty form field (means,
  // re-use the previous value). If this is the case, set it back.
  if (empty($partner->passphrase) && !empty($partner->id)) {
    // Reload the partner.
    $orig_partner = archibald_partner_load($partner->id);
    if (!empty($orig_partner->passphrase)) {
      $partner->passphrase = $orig_partner->passphrase;

      // This value is already encrypted. Skip the encryption down below.
      $already_encrypted = TRUE;
    }
  }

  // Set the entity name.
  $partner->name = "{$partner->partner_display_name[LANGUAGE_NONE][0]['value']} ({$partner->partner_username[LANGUAGE_NONE][0]['value']})";

  // If this partner is the new default, load the passed one(s), and update.
  if (!empty($partner->is_default)) {
    $default_partner = archibald_load_default_partner(TRUE);
    if ($default_partner && (empty($partner->id) || $default_partner->id !== $partner->id)) {
      $default_partner->is_default = 0;
      $default_partner->save();
      drupal_set_message(t("%partner was the previous default partner. It has now been replaced by %name.", array(
        '%partner' => $default_partner->name,
        '%name' => $partner->name,
      )), 'warning');
    }
  }

  // If this is a new partner, there won't be any IV set yet.
  if (!empty($partner->is_new)) {
    $partner->iv = base64_encode(openssl_random_pseudo_bytes(16));
  }

  // Encrypt the passphrase, if necessary.
  if (!empty($partner->passphrase) && !$already_encrypted) {
    $partner->passphrase = archibald_encrypt(
      $partner->passphrase,
      $partner->iv
    );
  }

  // Save.
  $partner->changed = REQUEST_TIME;
  $partner->save();

  if ($form_state['values']['update_remotely']) {
    // Update the partner data on the API.

    // Try to authenticate first. The partner may not be complete.
    try {
      $client = archibald_get_client_based_on_config($partner->id);

      // The partner is authenticated. Prepare the payload, and send it.
      $data = _archibald_prepare_partner_api_update($partner);

      if (!empty($partner->partner_logo[LANGUAGE_NONE][0]['fid'])) {
        if ($file = file_load($partner->partner_logo[LANGUAGE_NONE][0]['fid'])) {
          if (variable_get('archibald_host_images_on_api', FALSE)) {
            $filepath = drupal_realpath($file->uri);

            if (file_exists($filepath) && is_readable($filepath)) {
              $result = $client->uploadFile($filepath);
              $data['logo'] = $result['fileUrl'];
            }
          }
          else {
            $data['logo'] = file_create_url($file->uri);
          }
        }
      }

      try {
        $client->putPartner(
          $partner->partner_username[LANGUAGE_NONE][0]['value'],
          json_encode($data)
        );

        drupal_set_message(t("The partner data was successfully updated on the API."));
      }
      catch(Exception $e) {
        watchdog_exception('archibald', $e);

        drupal_set_message(t("An error occured when trying to update the partner data on the API. Please check the site logs, or contact your site administrator."), 'error');
      }
    }
    catch(Exception $e) {
      drupal_set_message(t("Could not authenticate partner with the API. Partner information was not updated."), 'warning');
    }
  }

  drupal_set_message(t('The Partner %name has been saved.', array('%name' => $partner->name)));
  $form_state['redirect'] = 'admin/archibald/partner';
}

/**
 * Helper function to encrypt passphrases.
 *
 * This function will base64 decode the passed IV, and base64 encode the
 * resulting encrypted data.
 *
 * @param string $data
 *    The data to encrypt.
 * @param string $id
 *    Base64 encoded initialization vector.
 *
 * @return string
 *    Base64 encoded, encrypted, data.
 */
function archibald_encrypt($data, $iv) {
  return base64_encode(openssl_encrypt(
    $data,
    ARCHIBALD_ENCRYPTION_CIPHER,
    archibald_get_encryption_key(),
    0,
    base64_decode($iv)
  ));
}

/**
 * Helper function to decrypt passphrases.
 *
 * This function will base64 decode the passed IV and data.
 *
 * @param string $data
 *    Base64 encoded data to decrypt.
 * @param string $id
 *    Base64 encoded initialization vector.
 *
 * @return string
 *    Decrypted data.
 */
function archibald_decrypt($data, $iv) {
  return openssl_decrypt(
    base64_decode($data),
    ARCHIBALD_ENCRYPTION_CIPHER,
    archibald_get_encryption_key(),
    0,
    base64_decode($iv)
  );
}

/**
 * Helper function to fetch the encryption key.
 *
 * @return string|false
 *    The encryption key, or false if it could not be loaded.
 */
function archibald_get_encryption_key() {
  $path = variable_get('archibald_encryption_key_path', NULL);
  if (file_exists($path) && is_readable($path)) {
    return base64_decode(file_get_contents($path));
  }
  return FALSE;
}

/**
 * Helper function to prepare data for updating a partner on the API.
 *
 * @param object $partner
 *    The partner entity.
 *
 * @return array
 *    An array, formatted for the payload to send to the API. This does not
 *    include the logo.
 */
function _archibald_prepare_partner_api_update($partner) {
  $data = array();
  foreach (array(
    'company' => 'partner_organization',
    'name' => 'partner_display_name',
    'country' => 'partner_country',
    'zip' => 'partner_zip',
    'city' => 'partner_city',
    'address' => 'partner_address',
    'address2' => 'partner_add_address',
    'address2' => 'partner_add_address',
    'website' => 'partner_url',
  ) as $key => $field_name) {
    $data[$key] = !empty($partner->{$field_name}[LANGUAGE_NONE][0]['value']) ?
      $partner->{$field_name}[LANGUAGE_NONE][0]['value'] :
      '';
  }

  // The email has a custom key.
  $data['email'] = !empty($partner->partner_email[LANGUAGE_NONE][0]['email']) ?
    $partner->partner_email[LANGUAGE_NONE][0]['email'] :
    '';

  return $data;
}

/**
 * Copy over the values of child checkboxes to the parent container.
 *
 * See the archibald_lomch_ontology_vocabulary_field_widget_form_alter()
 * function for an explanation of this callback.
 *
 * @see archibald_lomch_ontology_vocabulary_field_widget_form_alter()
 * @see _archibald_lomch_ontology_copy_checkbox_values()
 */
function archibald_lomch_ontology_copy_checkbox_values(&$element, $form_state) {
  module_load_include('inc', 'archibald', 'includes/archibald.field');
  _archibald_lomch_ontology_copy_checkbox_values($element, $form_state);
}

/**
 * Change the image field widget to put "Copyright" instead of "Title".
 *
 * See the lomch_preview_image_field_widget_form_alter() function for more
 * information.
 *
 * @see lomch_preview_image_field_widget_form_alter()
 * @see _archibald_lomch_preview_image_title_to_copyright()
 */
function archibald_lomch_preview_image_title_to_copyright($element, &$form_state, $form) {
  module_load_include('inc', 'archibald', 'includes/archibald.field');
  return _archibald_lomch_preview_image_title_to_copyright($element, $form_state, $form);
}

/**
 * Callback for generating the list of options for taxonomy fields.
 *
 * Fields that have an 'archibald_term_status' field can be filtered. Only terms
 * that have a value of '1' are shown on the node add/edit forms.
 *
 * @param array $field
 *    A field definition array.
 *
 * @return array
 *    An associative array of taxonomy terms, suitable for the options widget.
 */
function archibald_lomch_resource_taxonomy_options_list($field) {
  $options = array();

  foreach ($field['settings']['allowed_values'] as $tree) {
    if (!empty($tree['vocabulary'])) {
      // We only want to load the terms that have a archibald_term_status of 1.
      $query = new EntityFieldQuery;
      $result = $query
        ->entityCondition('entity_type', 'taxonomy_term')
        ->entityCondition('bundle', $tree['vocabulary'])
        ->fieldCondition('archibald_term_status', 'value', 1, '=')
        ->execute();

      if (!empty($result['taxonomy_term'])) {
        $terms = entity_load('taxonomy_term', array_keys($result['taxonomy_term']));
        foreach ($terms as $term) {
          $options[$term->tid] = $term->name;
        }
      }
    }
  }

  return $options;
}

/**
 * Get a client object.
 *
 * Returns a client object, implementing
 * Educa\DSB\Client\ApiClient\ClientInterface. The returned object will be
 * adapted for the configured API version.
 *
 * @param int $api_version
 *    The API version to use.
 * @param string $endpoint_url
 *    The URL to use for the REST api.
 * @param string $username
 *    The username to use for authentication.
 * @param int $key_fid
 *    The file ID of private key.
 * @param string $key_passphrase
 *    (optional) The passphrase for the private key.
 *
 * @return Educa\DSB\Client\ApiClient\ClientInterface
 *    API client object.
 */
function archibald_get_client($api_version, $endpoint_url, $username, $key_fid, $key_passphrase = '') {
  switch ($api_version) {
    case '__test':
      // This is used for unit tests.
      return new ArchibaldApiTestClient();
      break;

    default:
    case 2:
      $key_file = file_load($key_fid);
      $key_path = $key_file->uri;
      return new ArchibaldClient(
        $endpoint_url,
        $username,
        $key_path,
        $key_passphrase
      );
      break;
  }
}

/**
 * Get a client object based on the current settings.
 *
 * This is a helper function for archibald_get_client(). It will also
 * authenticate, or re-use an existing valid token, if any.
 *
 * @param int $partner_id
 *    (optional) An optional partner ID. If none is given, the default partner
 *    is loaded. Defaults to null.
 *
 * @return Educa\DSB\Client\ApiClient\ClientInterface
 *    API client object.
 */
function archibald_get_client_based_on_config($partner_id = NULL) {
  $api_version = variable_get('archibald_api_version', NULL);

  // If we are in a test scenario, skip the loading of a default partner.
  if ($api_version == '__test') {
    return new ArchibaldApiTestClient();
  }

  if (isset($partner_id)) {
    $partner = archibald_partner_load($partner_id);
  }
  else {
    $partner = archibald_load_default_partner();
  }

  $username = $partner->partner_username[LANGUAGE_NONE][0]['value'];
  $client = archibald_get_client(
    $api_version,
    variable_get('archibald_api_url', NULL),
    $username,
    $partner->partner_privatekey[LANGUAGE_NONE][0]['fid'],
    archibald_decrypt($partner->passphrase, $partner->iv)
  );

  $token = cache_get('archibald:auth_token:' . $username);
  if (!empty($token->data)) {
    $client->setTokenKey($token->data);
  }
  else {
    $client->reAuthenticate();
  }

  return $client;
}

/**
 * Check module dependencies.
 *
 * Helper function to check whether the Libraries API + X Autoload module, or
 * the Composer Manager module, are installed. We require either Composer
 * Manager, or Libraries API with X Autoload.
 *
 * @param bool $display_warning
 *    (optional) Whether to display a warning message to the user. Defaults to
 *    false.
 *
 * @return string|false
 *    Either "Composer Manager", "Libraries API + X Autoload" or false.
 */
function archibald_check_module_dependency($display_warning = FALSE) {
  if (module_exists('composer_manager')) {
    return "Composer Manager";
  }
  elseif (module_exists('libraries') && module_exists('xautoload')) {
    return "Libraries API + X Autoload";
  }
  else {
    if ($display_warning) {
      if (user_access('administer site configuration')) {
        $message = t("The Archibald module has unmet dependencies. Please check your site status under !link.", array(
          '!link' => l('admin/reports/status', 'admin/reports/status'),
        ));
      }
      else {
        $message = archibald_default_error_message();
      }

      drupal_set_message($message, 'warning', FALSE);
    }
    return FALSE;
  }
}

/**
 * Check module configuration.
 *
 * @param bool $display_warning
 *    (optional) Whether to display a warning message to the user. Defaults to
 *    false.
 * @return bool
 *    True if the module is correctly configured. False otherwise.
 */
function archibald_check_module_configuration($display_warning = FALSE) {
  // Check all required settings.
  foreach (array(
    'archibald_api_version',
    'archibald_api_url',
  ) as $key) {
    if (!variable_get($key, FALSE)) {
      if ($display_warning) {
        if (user_access('administer archibald')) {
          $message = t("The Archibald module is not correctly configured. Please verify your Archibald settings under !link.", array(
            '!link' => l('admin/config/services/archibald', 'admin/config/services/archibald'),
          ));
        }
        else {
          $message = archibald_default_error_message();
        }

        drupal_set_message($message, 'warning', FALSE);
      }
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Check default partner configuration.
 *
 * @param bool $display_warning
 *    (optional) Whether to display a warning message to the user. Defaults to
 *    false.
 * @return object|false
 *    The default partner if it is correctly configured. False otherwise.
 */
function archibald_check_default_partner_configuration($display_warning = FALSE) {
  $partner = archibald_load_default_partner();

  if (empty($partner)) {
    if ($display_warning) {
      if (user_access('administer archibald_partners')) {
        $message = t("The Archibald module requires 1 default partner, which has access to load Ontology data. Please verify your partner settings under !link.", array(
          '!link' => l('admin/archibald/partner', 'admin/archibald/partner'),
        ));
      }
      else {
        $message = archibald_default_error_message();
      }

      drupal_set_message($message, 'warning', FALSE);
    }
    return FALSE;
  }

  return $partner;
}

/**
 * Check third-party library availability.
 *
 * @param bool $display_warning
 *    (optional) Whether to display a warning message to the user. Defaults to
 *    false.
 *
 * @return bool
 *    True if the libraries are available. False otherwise.
 */
function archibald_check_libraries($display_warning = FALSE) {
  $library_installed = TRUE;
  foreach (archibald_libraries_info() as $library_name => $library_info) {
    if (module_exists('composer_manager')) {
      // This function can be called very early in the bootstrap phase, even
      // before Composer Manager registers the autoloader. Register it here so
      // we are sure we can access our classes. Composer Manager statically
      // checks if the autoloader is registered during boot, so it's safe to do
      // so; it won't initialize it again.
      composer_manager_register_autoloader();
      $library_installed = !empty($library_info['_archibald check class']) ?
        class_exists($library_info['_archibald check class']) :
        interface_exists($library_info['_archibald check interface']);
    }
    elseif (module_exists('libraries')) {
      $library = libraries_load($library_name);
      $library_installed = !empty($library['installed']);
    }

    if (!$library_installed) {
      // No point in checking further.
      break;
    }
  }

  // Finally, check OpenSSL support.
  if ($library_installed) {
    $library_installed = function_exists('openssl_get_privatekey');
  }

  if (!$library_installed) {
    if ($display_warning) {
      if (user_access('administer site configuration')) {
        $message = t("The Archibald module requires third-party libraries, but it cannot load them. Please check your site status under !link.", array(
          '!link' => l('admin/reports/status', 'admin/reports/status'),
        ));
      }
      else {
        $message = archibald_default_error_message();
      }

      drupal_set_message($message, 'warning', FALSE);
    }
    return FALSE;
  }
  else {
    return TRUE;
  }
}

/**
 * Check the module is usable.
 *
 * Check whether the module is usable. Optionally display warnings to the
 * user.
 *
 * @param bool $display_warning
 *    (optional) Whether to display a warning message to the user. Defaults to
 *    false.
 * @return bool
 *    True if the module is usable. False otherwise.
 */
function archibald_check_module_usable($display_warning = FALSE) {
  $dependency = !!archibald_check_module_dependency($display_warning);
  $libraries = archibald_check_libraries($display_warning);
  $config = archibald_check_module_configuration($display_warning);
  $partner_config = archibald_check_default_partner_configuration($display_warning);

  return $dependency && $libraries && $config && $partner_config;
}

/**
 * Get language fall back list.
 *
 * LOM-CH data has LangString fields, fields with different language keys. This
 * function returns a list of language keys, which the
 * Educa\DSB\Client\Lom\LomDescriptionInterface can use to search for an
 * available string.
 *
 * @see hook_archibald_language_fallback_list_alter()
 *
 * @return array
 *    An array of language codes, the current language being the first.
 */
function archibald_language_fallback_list() {
  $list = &drupal_static(__FUNCTION__);

  if (!isset($list)) {
    global $language;

    $list = array_unique(array(
      $language->language,
      'de',
      'fr',
      'it',
      'rm',
      'en',
    ));

    drupal_alter('archibald_language_fallback_list', $list);
  }

  return $list;
}

/**
 * Set the node hash.
 *
 * It is possible that a published description is changed by updating referenced
 * items, like a VCard or a Taxonomy term. In order to make sure we can flag
 * such outdated nodes for republishing, we calculate a MD5 hash of the
 * resulting JSON. If the JSON differs from the one that is currently published,
 * we know we need to re-publish it. We store 2 hashes, the "published" one,
 * which represents the values on the API, and the "current" hash, which
 * represents the latest state on this system.
 *
 * @param object $node
 *    The node to change the status for.
 * @param string $version
 *    (optional) The version of the hash. Either "current" or "published.
 *    Defaults to "current".
 */
function archibald_set_hash($node, $version = 'current') {
  if (!in_array($version, array('current', 'published'))) {
    $version = 'current';
  }

  $json = json_encode(archibald_json_export($node));

  db_merge('archibald_lom_hash')
    ->key(array('nid' => $node->nid))
    ->fields(array(
      'nid' => $node->nid,
      "{$version}_hash" => md5($json),
      'timestamp' => REQUEST_TIME,
    ))
    ->execute();
}

/**
 * Helper function for recomputing a hash without passing the full node.
 *
 * This is used for performance reasons in archibald_entity_update(). Instead
 * of having to load the full nodes (sometimes, hundreds) and adding them to
 * the queue for rehashing, it uses this function, and only passes the node nid.
 *
 * @see archibald_entity_update()
 * @see archibald_set_hash()
 */
function _archibald_recompute_hash($nid, $vid, $version = 'current') {
  archibald_set_hash(
    node_load($nid, $vid),
    $version
  );
}

/**
 * Add an item to the cron queue.
 *
 * Archibald uses the Queue API to store actions that need to be
 * (re)accomplished later. For example, when a publication of a node fails, it
 * will be queued to be tried again later. This function helps with the queuing
 * of these actions. Queue items must be grouped together using a common
 * naming schema, like "node:{nid}".
 *
 * @see archibald_queue_worker()
 * @see archibald_clear_queue()
 *
 * @param string $group
 *    The group this item will belong to. For example, a common group could be
 *    node:{nid}:{action}. This will allow us to group items together, and to
 *    clear items from the queue if necessary. Group names must be
 *    alphanumerical, and can contain colons (:), hyphens (-), and underscores
 *    (_).
 * @param string $callback
 *    The function to call when this item is to be executed.
 * @param array $args
 *    (optional) An array of arguments to pass to the callback. Defaults to an
 *    empty array.
 * @param string $include
 *    (optional) A file to include for the callback. Must be compatible with
 *    module_load_include(), where the file is a .inc file, and located within
 *    the Archibald module directory. Defaults to null.
 *
 * @ingroup archibald_queue_api
 */
function archibald_add_to_queue($group, $callback, $args = array(), $include = NULL) {
  DrupalQueue::get('archibald_queue')->createItem(array(
    'group' => $group,
    'callback' => $callback,
    'args' => $args,
    'include' => $include
  ));
}

/**
 * Clear items from the cron queue, optionally filtered by group.
 *
 * Queued items can be removed if they must not be executed anymore. It is also
 * possible to remove items belonging to a specific group.
 *
 * @see archibald_queue_worker()
 * @see archibald_add_to_queue()
 *
 * @param string $group
 *    (optional) An optional group name to clear the items for. See
 *    archibald_add_to_queue() for more information. Defaults to null.
 *
 * @ingroup archibald_queue_api
 */
function archibald_clear_queue($group = NULL) {
  $queue = DrupalQueue::get('archibald_queue');
  $release = array();

  if (isset($group)) {
    $regex = '/' . str_replace('*', '.+', $group) . '/';
  }

  while ($item = $queue->claimItem()) {
    if (
      (isset($regex) && preg_match($regex, $item->data['group'])) ||
      !isset($group)
    ) {
      $queue->deleteItem($item);
    }
    else {
      $release[] = $item;
    }
  }

  // If there are any items to release, release them now.
  if (!empty($release)) {
    foreach ($release as $item) {
      $queue->releaseItem($item);
    }
  }
}

/**
 * Cron queue worker callback.
 *
 * This is the callback defined in archibald_cron_queue_info(). It will process
 * items added to the queue using archibald_add_to_queue().
 *
 * @see archibald_add_to_queue()
 *
 * @ingroup archibald_queue_api
 */
function archibald_queue_worker($item_data) {
  if (isset($item_data['include'])) {
    module_load_include('inc', 'archibald', $item_data['include']);
  }

  if (isset($item_data['callback'])) {
    $callback = $item_data['callback'];
    if (function_exists($callback)) {
      $args = !empty($item_data['args']) ? $item_data['args'] : array();

      call_user_func_array($callback, $args);
    }
    else {
      throw new RuntimeException("Callback '$callback' on queue item is not a valid function. Data: " . print_r($item_data, 1));
    }
  }
  else {
    throw new RuntimeException("Didn't find any callback on queue item. Data: " . print_r($item_data, 1));
  }
}

/**
 * Export the node data to JSON format.
 *
 * The node properties and fields will be mapped to an object closely resembling
 * the expected JSON format for the dsb API. The returned value is no JSON
 * string, but an object that can easily be exported to JSON if necessary. Other
 * modules can alter the exported data and add values to it.
 *
 * @param object $node
 *    The description node to export. Has to be of archibald_lomch_description
 *    type.
 *
 * @return object|false
 *    The mapped data, ready for export, or false if the node is not of the
 *    archibald_lomch_description type.
 *
 * @see hook_archibald_json_export_alter()
 */
function archibald_json_export($node) {
  // Check the content type.
  if ($node->type != 'archibald_lomch_description') {
    return FALSE;
  }

  $data = &drupal_static(__FUNCTION__, array());

  if (!isset($data[$node->nid])) {
    module_load_include('inc', 'archibald', 'includes/archibald.publish');

    $data[$node->nid] = archibald_prepare_for_json_export($node);

    drupal_alter('archibald_json_export', $data[$node->nid], $node);
  }

  return $data[$node->nid];
}

/**
 * Page callback for adding a custom entity.
 *
 * The Entity module sets a page title based on the entity type, but transforms
 * it to lowercase. This is fine in most languages, but is wrong in German.
 * Alter the page callback for adding one of our custom entities. Reset the
 * page title to something coherent.
 *
 * @see archibald_menu_alter()
 * @see entity_ui_get_form()
 */
function archibald_custom_entity_add_page($entity_type, $entity, $op) {
  $form = entity_ui_get_form($entity_type, $entity, $op);
  switch ($entity_type) {
    case 'archibald_partner':
      drupal_set_title(t("Add partner"));
      break;
    case 'archibald_vcard':
      drupal_set_title(t("Add VCard"));
      break;
  }
  return $form;
}

/**
 * Page callback for viewing a VCard entity.
 */
function archibald_vcard_view($id) {
  $vcard = archibald_vcard_load($id);
  drupal_set_title($vcard->name);
  return entity_view('archibald_vcard', array($vcard));
}

/**
 * Load an archibald_vcard.
 *
 * @param int $id
 *    The VCard ID to load.
 *
 * @return object|null
 *    The VCard, or null if it doesn't exist.
 */
function archibald_vcard_load($id) {
  $vcards = entity_load('archibald_vcard', array($id));
  return isset($vcards[$id]) ? $vcards[$id] : NULL;
}

/**
 * Page callback for viewing a Partner entity.
 */
function archibald_partner_page_view($id) {
  $partner = archibald_partner_load($id);
  drupal_set_title($partner->name);
  return entity_view('archibald_partner', array($partner));
}

/**
 * Load an archibald_partner.
 *
 * @param int $id
 *    The partner ID to load.
 * @param bool $reset
 *    (optional) Whether to reset the internal cache. Defaults to false.
 *
 * @return object|null
 *    The partner, or null if it doesn't exist.
 */
function archibald_partner_load($id, $reset = FALSE) {
  $cache = &drupal_static(__FUNCTION__, array());
  if (!isset($cache[$id]) || $reset) {
    $partners = entity_load('archibald_partner', array($id), array(), $reset);
    $cache[$id] = $partners[$id];
  }
  return isset($cache[$id]) ? $cache[$id] : NULL;
}
/**
 * Load an archibald_partner by name.
 *
 * @param string $name
 *    The partner name to load.
 * @param bool $reset
 *    (optional) Whether to reset the internal cache. Defaults to false.
 *
 * @return object|null
 *    The partner, or null if it doesn't exist.
 */
function archibald_partner_load_by_name($name, $reset = FALSE) {
  $cache = &drupal_static(__FUNCTION__, array());
  if (!isset($cache[$name]) || $reset) {
    $query = new EntityFieldQuery();
    $query->entityCondition('entity_type', 'archibald_partner')
          ->fieldCondition('partner_username', 'value', $name, '=');

    $result = $query->execute();

    if (!empty($result['archibald_partner'])) {
      $cache[$name] = archibald_partner_load(
        @reset(array_keys($result['archibald_partner']))
      );
    }
  }
  return isset($cache[$name]) ? $cache[$name] : NULL;
}

/**
 * Load all archibald_partners.
 *
 * There seems to be an issue with entity_load's internal cache. Calling the
 * function several times in the same request sometimes returns empty results.
 * Implement our own static cache to circumvent the problem.
 *
 * @param bool $reset
 *    (optional) Whether to reset the internal cache. Defaults to false.
 *
 * @return array
 *    The partners.
 */
function archibald_partner_load_all($reset = FALSE) {
  $cache = &drupal_static(__FUNCTION__);
  if (empty($cache) || $reset) {
    $cache = entity_load('archibald_partner', FALSE, array(), TRUE);
  }
  return $cache;
}

/**
 * Load the default partner.
 *
 * @param bool $reset
 *    (optional) Resets the internal cache. Defaults to false.
 *
 * @return object|null
 *    The default archibald_partner entity, or null if none is specified.
 */
function archibald_load_default_partner($reset = FALSE) {
  $partner = &drupal_static(__FUNCTION__, NULL);

  if (!isset($partner) || $reset) {
    // Make sure we reset it, in case $reset is true.
    $partner = NULL;

    $result = db_select('archibald_partner', 'a')
      ->fields('a', array('id'))
      ->condition('is_default', 1)
      ->execute()
      ->fetchField();

    if (!empty($result)) {
      // If we want to reset the internal cache for the default partner, we
      // might as well reset the entity cache. Pass reset to the load function.
      $partner = archibald_partner_load($result, $reset);
    }
  }

  return $partner;
}

/**
 * Load a archibald_lomch_description based on its LOM ID.
 *
 * @param string $lom_id
 *    The archibald_lomch_description LOM ID.
 *
 * @return object
 *    The archibald_lomch_description node.
 */
function archibald_lomch_description_load_by_lom_id($lom_id) {
  $cache = &drupal_static(__FUNCTION__, array());

  if (!isset($cache[$lom_id])) {
    $nid = db_select('archibald_lom_id', 'a')
              ->fields('a', array('nid'))
              ->condition('lom_id', $lom_id)
              ->execute()
              ->fetchField();

    $cache[$lom_id] = !empty($nid) ? node_load($nid) : NULL;
  }

  return $cache[$lom_id];
}

/**
 * Page callback for viewing a description JSON export.
 */
function archibald_page_node_json_view($node) {
  // Load the latest revision.
  $vid_to_load = revisioning_get_latest_revision_id($node->nid);
  if ($vid_to_load != $node->vid) {
    drupal_set_message(t("This page displays the data for the latest, pending revision."), 'warning');
    $node = node_load($node->nid, $vid_to_load);
  }

  $data = archibald_json_export($node);

  // If we don't host the images ourselves, the JSON will not contain any
  // previewImage data. But because this is only a preview, it might be helpful
  // to show what it would look like once published. So, we mimick the API
  // behavior, and add the preview image.
  if (variable_get('archibald_host_images_on_api', FALSE) &&!empty($node->lomch_preview_image)) {
    $file = file_load($node->lomch_preview_image[LANGUAGE_NONE][0]['fid']);
    if ($file) {
      $url = rtrim(variable_get('archibald_api_url', NULL), '/');
      $url .= '/file/default/' . md5_file($file->uri) . '.' . @end(explode('.', $file->uri));
      $data->technical->previewImage = (object) array(
        'image' => $url,
      );

      if (!empty($node->lomch_preview_image[LANGUAGE_NONE][0]['title'])) {
        $data->technical->previewImage->copyright = $node->lomch_preview_image[LANGUAGE_NONE][0]['title'];
      }
    }
  }

  return '<pre>' . json_encode($data, JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_QUOT) . '</pre>';
}

/**
 * Page callback for viewing a description validation errors.
 */
function archibald_page_node_validation_view($node) {
  // Load the latest revision.
  $vid_to_load = revisioning_get_latest_revision_id($node->nid);
  if ($vid_to_load != $node->vid) {
    drupal_set_message(t("This page displays the data for the latest, pending revision."), 'warning');
    $node = node_load($node->nid, $vid_to_load);
  }

  if (empty($node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id'])) {
    return '<div class="messages error">' . t("%title cannot be validated. You must first assign it to a partner.", array(
      '%title' => $node->title,
    )) . '</div>';
  }

  module_load_include('inc', 'archibald', 'includes/archibald.publish');
  $result = archibald_validate_description($node);

  // The description is valid.
  if (!empty($result['valid'])) {
    return '<div class="messages status">' . t("%title is valid. There are no issues to display.", array(
      '%title' => $node->title,
    )) . '</div>';
  }
  elseif (empty($result)) {
    return '<div class="messages error">' . t("Couldn't validate %title. Please try again later.", array(
      '%title' => $node->title,
    )) . '</div>';
  }
  else {
    module_load_include('inc', 'archibald', 'includes/archibald.entities');
    $fields = archibald_archibald_lomch_description_fields();

    // First, we need to match the field names to the errors. Prepare a list of
    // checks, based on the field data.
    $regexes = array();
    foreach ($fields as $field_name => $field_info) {
      if (isset($field_info['api_name'])) {
        if (!is_array($field_info['api_name'])) {
          $field_info['api_name'] = array($field_info['api_name']);
        }

        $regex = implode('|', array_map(function($api_name) {
          return str_replace(
            array('.', '*'),
            array('\.', '.*'),
            $api_name
          );
        }, $field_info['api_name']));

        $regexes["/($regex)/"] = array(
          'field_name' => $field_name,
          'field_label' => $field_info['label'],
        );
      }
    }

    // Now, match each validation error to a field name thanks to the regular
    // expression. We will group them together, in case multiple errors apply
    // to the same field.
    $errors = array('unknown' => array(
      'field_label' => t("unknown"),
    ));
    foreach ($result['errors'] as $key => $value) {
      foreach ($regexes as $regex => $field_info) {
        // Check if the error matches one of our fields.
        if (preg_match($regex, $key)) {
          // Do we already have our list? If not, initialize it.
          if (!isset($errors[$field_info['field_name']])) {
            $errors[$field_info['field_name']] = array(
              'field_label' => $field_info['field_label'],
              'errors' => array(),
            );
          }

          // Add the error to the list, and continue.
          $errors[$field_info['field_name']]['errors'][$key] = is_array($value) ? implode(', ', $value) : $value;
          continue 2;
        }
      }

      // Failsafe. We couldn't match the error with any field name. Add it
      // anyway.
      $errors['unknown']['errors'][$key] = is_array($value) ? implode(', ', $value) : $value;
    }

    $context = array(
      'node' => $node,
      'fields' => $fields,
      'result' => $result,
    );
    drupal_alter('archibald_validation_report', $errors, $context);

    // We can now construct our table.
    $rows = array();
    foreach ($errors as $field_name => $info) {
      if (empty($info['errors'])) {
        continue;
      }

      $row = array();
      $row[] = array(
        'data' => t("@field_label (%field_name)", array(
          '@field_label' => function_exists('i18n_string') ?
            i18n_string("field:{$field_name}:archibald_lomch_description:label", $info['field_label']) :
            $info['field_label'],
          '%field_name' => $field_name,
        )),
        'rowspan' => count($info['errors']),
      );
      $keys = array_keys($info['errors']);
      $row[] = t("%error (applies to: %key)", array(
        '%error' => array_shift($info['errors']),
        '%key' => array_shift($keys),
      ));
      $rows[] = $row;

      // Are there more errors to display?
      if (count($info['errors'])) {
        foreach ($info['errors'] as $key => $error) {
          $rows[] = t("%error (applies to: %key)", array(
            '%error' => $key,
            '%key' => $error,
          ));
        }
      }
    }

    return theme('table', array(
      'header' => array(
        t("Field", array(), array('context' => 'archibald:validate')),
        t("Error(s)", array(), array('context' => 'archibald:validate')),
      ),
      'rows' => $rows,
    ));
  }
}

/**
 * Helper function to generate a description text for a VCard field.
 *
 * For the VCard entity reference fields, we want to provide a help text with a
 * link, so users can quickly understand where to manage VCards. This
 * description text also checks if the user has access. If not, the text is
 * different.
 *
 * @param object $account
 *    The user for which the description text should be shown.
 *
 * @return string
 *    The generated description text.
 */
function archibald_generate_vcard_help_text($account) {
  if (user_access('administer archibald_vcards', $account)) {
    $entity_info = archibald_entity_info();
    return t("You can manage existing VCards <a href='!link' target='_blank'>here</a>. Click on the <em>Create VCard</em> link to create a new one.", array(
      '!link' => url($entity_info['archibald_vcard']['admin ui']['path']),
    ));
  }
  else {
    return '';
  }
}

/**
 * Add custom admin theme styles.
 *
 * We have special styles for certain admin themes, to make them look a
 * bit better for Archibald. Make sure the admin theme is active, that it is
 * active for node forms, and that the user has access to it.
 *
 * @param bool $check_node_forms
 *    (optional) Whether to check if the theme is active for node forms.
 *    Defaults to false.
 */
function archibald_add_admin_theme_styles($check_node_forms = FALSE) {
  if (
    variable_get('archibald_enhance_admin_themes', 0) &&
    in_array(variable_get('admin_theme', 0), array('seven', 'adminimal')) &&
    (!$check_node_forms || variable_get('node_admin_theme', 0)) &&
    user_access('view the administration theme')
  ) {
    $path = drupal_get_path('module', 'archibald');
    drupal_add_css("$path/css/archibald.admin_theme.css");
    drupal_add_css("$path/css/archibald.admin_theme." . variable_get('admin_theme', 0) . '.css');
  }
}

/**
 * Check if any descriptions need to be republished.
 *
 * @return int
 *    The number of descriptions that need to be republished.
 */
function archibald_lomch_description_num_require_republishing() {
  $result = &drupal_static(__FUNCTION__);

  if (!isset($result)) {
    $query = db_select('archibald_lom_hash', 'a');
    $query->leftJoin('node', 'n', 'n.nid = a.nid');
    $result = $query->where('a.current_hash <> a.published_hash')
                      ->condition('n.status', NODE_PUBLISHED)
                      ->countQuery()
                      ->execute()
                      ->fetchField();
  }

  return $result;
}

/**
 * Default error message.
 *
 * This is to prevent typing the same error message all over the place. DRYer.
 *
 * @return string
 *    The default error message when something is wrong with the module.
 */
function archibald_default_error_message() {
  return t("The Archibald module is not correctly configured. Please contact your site administrator.");
}

/**
 * Helper function to get the revision status.
 *
 * Fetch a human-readable status message. The status message is computed in
 * context of the entire node life cycle.
 *
 * @param object $node
 *
 * @return string
 *    The status message.
 */
function archibald_get_revision_status($node) {
  return $node->status != NODE_PUBLISHED ?
    ARCHIBALD_REVISION_STATE_NOT_PUBLISHED :
    (
      !$node->is_current ?
        (
          !$node->is_pending ?
            ARCHIBALD_REVISION_STATE_NEWER_PUBLISHED :
            ARCHIBALD_REVISION_STATE_OLDER_PUBLISHED
        ) :
        (
          revisioning_get_latest_revision_id($node->nid) == $node->vid ?
            ARCHIBALD_REVISION_STATE_CURRENT :
            ARCHIBALD_REVISION_STATE_CURRENT_NEW_DRAFT
        )
    );
}

/**
 * Prevent a node from dropping in "archive limbo".
 *
 * Is this the latest revision, and are there more than 1 revision? If so,
 * the revision will fall into the "archived" limbo, which Archibald is not
 * prepared for. If this is the case, trick Drupal into thinking the
 * previous revision is the active one. This will turn the current one into
 * a "pending" one in Revisioning's eyes. This will allow our logic to work,
 * and will prevent confusion for our users.
 *
 * @param object $node
 *    The unpublished node (revision).
 */
function archibald_lomch_description_save_from_archive_limbo($node) {
  if (
    $node->num_revisions > 1 &&
    $node->is_current &&
    $node->status == NODE_NOT_PUBLISHED
  ) {
    watchdog('archibald:content', "%title was unpublished, and risks falling into archive limbo. Correct it.", array(
      '%title' => $node->title,
    ), WATCHDOG_INFO);

    $prev_vid = db_query("
      SELECT  vid
        FROM  {node_revision}
       WHERE  nid = :nid
    ORDER BY  vid DESC
       LIMIT  1, 1
    ", array(':nid' => $node->nid))->fetchField();

    $prev_revision = node_load($node->nid, $prev_vid);

    db_update('node')
      ->fields(array(
        'vid'     => $prev_vid,
        'title'   => $prev_revision->title,
      ))
      ->condition('nid', $node->nid)
      ->execute();

    db_update('node_revision')
      ->fields(array(
        'status'  => NODE_NOT_PUBLISHED,
      ))
      ->condition('vid', $prev_vid)
      ->execute();
  }
}
