You are here

opigno_scorm.manifest.inc in Opigno 7

Manifest file extraction logic.

Contains functions for parsing the manifest XML, treating the data recursively and preparing the data for storage. Only data that is relevant to a SCORM player is stored. For example, resource information is not stored.

File

modules/scorm/includes/opigno_scorm.manifest.inc
View source
<?php

/**
 * @file
 * Manifest file extraction logic.
 *
 * Contains functions for parsing the manifest XML, treating the data recursively and
 * preparing the data for storage. Only data that is relevant to a SCORM player is stored.
 * For example, resource information is not stored.
 */

// XML tag names
define('OPIGNO_SCORM_MANIFEST_METADATA', 'METADATA');
define('OPIGNO_SCORM_MANIFEST_ORGANIZATIONS', 'ORGANIZATIONS');
define('OPIGNO_SCORM_MANIFEST_ORGANIZATION', 'ORGANIZATION');
define('OPIGNO_SCORM_MANIFEST_RESOURCES', 'RESOURCES');
define('OPIGNO_SCORM_MANIFEST_RESOURCE', 'RESOURCE');
define('OPIGNO_SCORM_MANIFEST_TITLE', 'TITLE');
define('OPIGNO_SCORM_MANIFEST_ITEM', 'ITEM');
define('OPIGNO_SCORM_MANIFEST_SEQUENCING', 'IMSSS:SEQUENCING');
define('OPIGNO_SCORM_MANIFEST_CTRL_MODE', 'IMSSS:CONTROLMODE');
define('OPIGNO_SCORM_MANIFEST_OBJECTIVES', 'IMSSS:OBJECTIVES');
define('OPIGNO_SCORM_MANIFEST_OBJECTIVE', 'IMSSS:OBJECTIVE');
define('OPIGNO_SCORM_MANIFEST_PRIMARY_OBJECTIVE', 'IMSSS:PRIMARYOBJECTIVE');
define('OPIGNO_SCORM_MANIFEST_MIN_NORMALIZED_MEASURE', 'IMSSS:MINNORMALIZEDMEASURE');
define('OPIGNO_SCORM_MANIFEST_MAX_NORMALIZED_MEASURE', 'IMSSS:MAXNORMALIZEDMEASURE');

// XML attributes
define('OPIGNO_SCORM_MANIFEST_DEFAULT_ATTR', 'DEFAULT');
define('OPIGNO_SCORM_MANIFEST_ID_ATTR', 'IDENTIFIER');
define('OPIGNO_SCORM_MANIFEST_REFID_ATTR', 'IDENTIFIERREF');
define('OPIGNO_SCORM_MANIFEST_LAUNCH_ATTR', 'LAUNCH');
define('OPIGNO_SCORM_MANIFEST_PARAM_ATTR', 'PARAMETERS');
define('OPIGNO_SCORM_MANIFEST_TYPE_ATTR', 'TYPE');
define('OPIGNO_SCORM_MANIFEST_SCORM_TYPE_ATTR', 'ADLCP:SCORMTYPE');
define('OPIGNO_SCORM_MANIFEST_HREF_ATTR', 'HREF');
define('OPIGNO_SCORM_MANIFEST_MANIFEST_ATTR', 'IDENTIFIER');
define('OPIGNO_SCORM_MANIFEST_CHOICE_ATTR', 'CHOICE');
define('OPIGNO_SCORM_MANIFEST_FLOW_ATTR', 'FLOW');
define('OPIGNO_SCORM_MANIFEST_CHOICE_EXIT_ATTR', 'CHOICEEXIT');
define('OPIGNO_SCORM_MANIFEST_OBJECTIVE_ID_ATTR', 'OBJECTIVEID');
define('OPIGNO_SCORM_MANIFEST_OBJECTIVE_SATISFIED_BY_MEASURE_ATTR', 'SATISFIEDBYMEASURE');

/**
 * Extract the SCORM package from the file.
 *
 * Given a file ID (which should be a valid SCORM ZIP file), this function extracts
 * the ZIP to OPIGNO_SCORM_DIRECTORY, finds the manifest file and parses it.
 * Once parsed, it stores the manifest data.
 *
 * Returns the id of the extracted SCORM package, or FALSE if an error occurs.
 *
 * @param int $fid
 *        The file ID that we'll extract.
 *
 * @return int|false
 */
function opigno_scorm_extract($fid) {
  $file = file_load($fid);
  $path = drupal_realpath($file->uri);
  $zip = new ZipArchive();
  $result = $zip
    ->open($path);
  if ($result === TRUE) {
    $extract_dir = OPIGNO_SCORM_DIRECTORY . '/scorm_' . $fid;
    $zip
      ->extractTo($extract_dir);
    $zip
      ->close();

    // This is a standard: the manifest file will always be here.
    $manifest_file = $extract_dir . '/imsmanifest.xml';
    if (file_exists($manifest_file)) {

      // Prepare the Scorm DB entry.
      $scorm = (object) array(
        'fid' => $fid,
        'extracted_dir' => $extract_dir,
        'manifest_file' => $manifest_file,
        'manifest_id' => '',
        'metadata' => '',
      );

      // Parse the manifest file and extract the data.
      $manifest_data = opigno_scorm_extract_manifest_data($manifest_file);

      // Get the manifest ID, if it's given.
      if (!empty($manifest_data['manifest_id'])) {
        $scorm->manifest_id = $manifest_data['manifest_id'];
      }

      // If the file contains (global) metadata, serialize it.
      if (!empty($manifest_data['metadata'])) {
        $scorm->metadata = serialize($manifest_data['metadata']);
      }

      // Try saving the SCORM to the DB.
      if (opigno_scorm_scorm_save($scorm)) {

        // Store each SCO.
        if (!empty($manifest_data['scos']['items'])) {
          foreach ($manifest_data['scos']['items'] as $i => $sco_item) {
            $sco = (object) array(
              'scorm_id' => $scorm->id,
              'organization' => $sco_item['organization'],
              'identifier' => $sco_item['identifier'],
              'parent_identifier' => $sco_item['parent_identifier'],
              'launch' => $sco_item['launch'],
              'type' => $sco_item['type'],
              'scorm_type' => $sco_item['scorm_type'],
              'title' => $sco_item['title'],
              'weight' => empty($sco_item['weight']) ? $sco_item['weight'] : 0,
              'attributes' => $sco_item['attributes'],
            );
            if (opigno_scorm_sco_save($sco)) {

              // @todo Store SCO attributes.
            }
            else {
              watchdog('opigno_scorm', "An error occured when saving an SCO.", array(), WATCHDOG_ERROR);
            }
          }
        }
        return TRUE;
      }
      else {
        watchdog('opigno_scorm', "An error occured when saving the SCORM package data.", array(), WATCHDOG_ERROR);
      }
    }
  }
  else {
    $error = 'none';
    switch ($result) {
      case ZipArchive::ER_EXISTS:
        $error = 'ER_EXISTS';
        break;
      case ZipArchive::ER_INCONS:
        $error = 'ER_INCONS';
        break;
      case ZipArchive::ER_INVAL:
        $error = 'ER_INVAL';
        break;
      case ZipArchive::ER_NOENT:
        $error = 'ER_NOENT';
        break;
      case ZipArchive::ER_NOZIP:
        $error = 'ER_NOZIP';
        break;
      case ZipArchive::ER_OPEN:
        $error = 'ER_OPEN';
        break;
      case ZipArchive::ER_READ:
        $error = 'ER_READ';
        break;
      case ZipArchive::ER_SEEK:
        $error = 'ER_SEEK';
        break;
    }
    watchdog('opigno_scorm', "An error occured when unziping the SCORM package data. Error: !error", array(
      '!error' => $error,
    ), WATCHDOG_ERROR);
  }
  return FALSE;
}

/**
 * Extract the manifest data.
 *
 * Parse the manifest XML with XML2Array (located in includes/XML2Array.php)
 * and extract data that is relevant to us.
 *
 * @param string $manifest_file
 *
 * @return array
 */
function opigno_scorm_extract_manifest_data($manifest_file) {
  $data = array();

  // Get the XML as a string.
  $manifest_string = file_get_contents($manifest_file);

  // Parse it as an array.
  $parser = new XML2Array();
  $manifest = $parser
    ->parse($manifest_string);

  // The parser returns an array of arrays - skip the first element.
  $manifest = array_shift($manifest);

  // Get the manifest ID, if any.
  if (!empty($manifest['attrs'][OPIGNO_SCORM_MANIFEST_MANIFEST_ATTR])) {
    $data['manifest_id'] = $manifest['attrs'][OPIGNO_SCORM_MANIFEST_MANIFEST_ATTR];
  }
  else {
    $data['manifest_id'] = '';
  }

  // Extract the global metadata information.
  $data['metadata'] = opigno_scorm_extract_manifest_metadata($manifest);

  // Extract the SCOs (course items). Gets the default SCO and a list of all SCOs.
  $data['scos'] = opigno_scorm_extract_manifest_scos($manifest);

  // Extract the resources, so we can combine the SCOs and resources.
  $data['resources'] = opigno_scorm_extract_manifest_resources($manifest);

  // Combine the resources and SCOs.
  $data['scos']['items'] = opigno_scorm_combine_manifest_sco_and_resources($data['scos']['items'], $data['resources']);
  return $data;
}

/**
 * Extract the manifest metadata.
 *
 * Find the metadata of this manifest file and return it.
 * We only treat global metadata - we don't parse metadata on SCOs or
 * resources.
 *
 * @param array $manifest
 *
 * @return array
 */
function opigno_scorm_extract_manifest_metadata($manifest) {
  foreach ($manifest['children'] as $child) {
    if ($child['name'] == OPIGNO_SCORM_MANIFEST_METADATA) {
      $meta = array();
      foreach ($child['children'] as $metadata) {
        $meta[strtolower($metadata['name'])] = $metadata['tagData'];
      }
      return $meta;
    }
  }
  return array();
}

/**
 * Extract the manifest SCO items.
 *
 * @see _opigno_scorm_extract_manifest_scos_items().
 *
 * @param array $manifest
 *
 * @return array
 *        'items' => array of SCOs
 *        'default' => default SCO identifier
 */
function opigno_scorm_extract_manifest_scos($manifest) {
  $items = array(
    'items' => array(),
  );
  foreach ($manifest['children'] as $child) {
    if ($child['name'] == OPIGNO_SCORM_MANIFEST_ORGANIZATIONS) {
      if (!empty($child['attrs'][OPIGNO_SCORM_MANIFEST_DEFAULT_ATTR])) {
        $items['default'] = $child['attrs'][OPIGNO_SCORM_MANIFEST_DEFAULT_ATTR];
      }
      else {
        $items['default'] = '';
      }
      $items['items'] = array_merge(_opigno_scorm_extract_manifest_scos_items($child['children']), $items['items']);
    }
  }
  return $items;
}

/**
 * Helper function to recursively extract the manifest SCO items.
 *
 * The data is extracted as a flat array - it contains to hierarchy. Because of this,
 * the items are not extracted in logical order. However, each "level" is given a weight
 * which allows us to know how to organize them.
 *
 * @param array $manifest
 * @param string|int $parent_identifier = 0
 * @param string $organization = ''
 *
 * @return array
 */
function _opigno_scorm_extract_manifest_scos_items($manifest, $parent_identifier = 0, $organization = '') {
  $items = array();
  $weight = 0;
  foreach ($manifest as $item) {
    if (in_array($item['name'], array(
      OPIGNO_SCORM_MANIFEST_ORGANIZATION,
      OPIGNO_SCORM_MANIFEST_ITEM,
    )) && !empty($item['children'])) {
      $attributes = array();
      if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_ID_ATTR])) {
        $identifier = $item['attrs'][OPIGNO_SCORM_MANIFEST_ID_ATTR];
      }
      else {
        $identifier = uniqid();
      }
      if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_LAUNCH_ATTR])) {
        $launch = $item['attrs'][OPIGNO_SCORM_MANIFEST_LAUNCH_ATTR];
      }
      else {
        $launch = '';
      }
      if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_REFID_ATTR])) {
        $resource_identifier = $item['attrs'][OPIGNO_SCORM_MANIFEST_REFID_ATTR];
      }
      else {
        $resource_identifier = '';
      }
      if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_PARAM_ATTR])) {
        $attributes['parameters'] = $item['attrs'][OPIGNO_SCORM_MANIFEST_PARAM_ATTR];
      }
      if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_TYPE_ATTR])) {
        $type = $item['attrs'][OPIGNO_SCORM_MANIFEST_TYPE_ATTR];
      }
      else {
        $type = '';
      }
      if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_SCORM_TYPE_ATTR])) {
        $scorm_type = $item['attrs'][OPIGNO_SCORM_MANIFEST_SCORM_TYPE_ATTR];
      }
      else {
        $scorm_type = '';
      }

      // Find the title, which is also a child node.
      foreach ($item['children'] as $child) {
        if ($child['name'] == OPIGNO_SCORM_MANIFEST_TITLE) {
          $title = $child['tagData'];
          break;
        }
      }

      // Find any sequencing control modes, which are also child nodes.
      $control_modes = array();
      foreach ($item['children'] as $child) {
        if ($child['name'] == OPIGNO_SCORM_MANIFEST_SEQUENCING) {
          $control_modes = opigno_scorm_extract_item_sequencing_control_modes($child);
          $attributes['objectives'] = opigno_scorm_extract_item_sequencing_objectives($child);
        }
      }

      // Failsafe - we cannot have elements without a title.
      if (empty($title)) {
        $title = 'NO TITLE';
      }
      $items[] = array(
        'manifest' => '',
        // @deprecated
        'organization' => $organization,
        'title' => $title,
        'identifier' => $identifier,
        'parent_identifier' => $parent_identifier,
        'launch' => $launch,
        'resource_identifier' => $resource_identifier,
        'type' => $type,
        'scorm_type' => $scorm_type,
        'weight' => $weight,
        'attributes' => $control_modes + $attributes,
      );

      // The first item is not an "item", but an "organization" node. This is the organization
      // for the remainder of the tree. Get it, and pass it along, so we know to which organization
      // the SCOs belong.
      if (empty($organization) && $item['name'] == OPIGNO_SCORM_MANIFEST_ORGANIZATION) {
        $organization = $identifier;
      }

      // Recursively get child items, and merge them to get a flat list.
      $items = array_merge(_opigno_scorm_extract_manifest_scos_items($item['children'], $identifier, $organization), $items);
    }
    $weight++;
  }
  return $items;
}

/**
 * Extract the manifest SCO item sequencing objective.
 *
 * This extracts sequencing objectives from an item. Objectives allow the system
 * to know how to "grade" the SCORM object.
 *
 * @param array $item_manifest
 *
 * @return array
 */
function opigno_scorm_extract_item_sequencing_objectives($item_manifest) {
  $objectives = array();
  foreach ($item_manifest['children'] as $child) {
    if ($child['name'] == OPIGNO_SCORM_MANIFEST_OBJECTIVES) {
      foreach ($child['children'] as $child_objective) {
        if (!empty($child_objective['attrs'][OPIGNO_SCORM_MANIFEST_OBJECTIVE_ID_ATTR])) {
          $id = $child_objective['attrs'][OPIGNO_SCORM_MANIFEST_OBJECTIVE_ID_ATTR];
        }
        else {
          $id = uniqid();
        }
        if ($child_objective['name'] == OPIGNO_SCORM_MANIFEST_PRIMARY_OBJECTIVE) {

          // Note: boolean attributes are stored as a strings. PHP does not know
          // how to cast 'false' to FALSE. Use string comparisons to bypass
          // this limitation by PHP. See below.
          $satisfied_by_measure = FALSE;
          if (!empty($child_objective['attrs'][OPIGNO_SCORM_MANIFEST_OBJECTIVE_SATISFIED_BY_MEASURE_ATTR])) {
            $satisfied_by_measure = strtolower($child_objective['attrs'][OPIGNO_SCORM_MANIFEST_OBJECTIVE_SATISFIED_BY_MEASURE_ATTR]) === 'true';
          }
          $objective = array(
            'primary' => TRUE,
            'secondary' => FALSE,
            'id' => $id,
            'satisfied_by_measure' => $satisfied_by_measure,
          );
          foreach ($child_objective['children'] as $primary_obj_child) {
            if ($primary_obj_child['name'] == OPIGNO_SCORM_MANIFEST_MIN_NORMALIZED_MEASURE) {
              $objective['min_normalized_measure'] = $primary_obj_child['tagData'];
            }
            elseif ($primary_obj_child['name'] == OPIGNO_SCORM_MANIFEST_MAX_NORMALIZED_MEASURE) {
              $objective['max_normalized_measure'] = $primary_obj_child['tagData'];
            }
          }
          $objectives[] = $objective;
        }
        elseif ($child_objective['name'] == OPIGNO_SCORM_MANIFEST_OBJECTIVE) {
          $objectives[] = array(
            'primary' => FALSE,
            'secondary' => TRUE,
            'id' => $id,
          );
        }
      }
    }
  }
  return $objectives;
}

/**
 * Extract the manifest SCO item sequencing control modes.
 *
 * This extracts sequencing control modes from an item. Control modes
 * describe how the user can navigate around the course
 * (e.g.: display the tree or not, skip SCOs, etc).
 *
 * @param array $item_manifest
 *
 * @return array
 */
function opigno_scorm_extract_item_sequencing_control_modes($item_manifest) {
  $defaults = array(
    'control_mode_choice' => TRUE,
    'control_mode_flow' => FALSE,
    'control_mode_choice_exit' => TRUE,
    'control_mode_forward_only' => FALSE,
  );
  $control_modes = array();
  foreach ($item_manifest['children'] as $child) {
    if ($child['name'] == OPIGNO_SCORM_MANIFEST_CTRL_MODE) {

      // Note: boolean attributes are stored as a strings. PHP does not know
      // how to cast 'false' to FALSE. Use string comparisons to bypass
      // this limitation by PHP. See below.
      if (!empty($child['attrs'][OPIGNO_SCORM_MANIFEST_CHOICE_ATTR])) {
        $control_modes['control_mode_choice'] = strtolower($child['attrs'][OPIGNO_SCORM_MANIFEST_CHOICE_ATTR]) === 'true';
      }
      if (!empty($child['attrs'][OPIGNO_SCORM_MANIFEST_FLOW_ATTR])) {
        $control_modes['control_mode_flow'] = strtolower($child['attrs'][OPIGNO_SCORM_MANIFEST_FLOW_ATTR]) === 'true';
      }
      if (!empty($child['attrs'][OPIGNO_SCORM_MANIFEST_CHOICE_EXIT_ATTR])) {
        $control_modes['control_mode_choice_exit'] = strtolower($child['attrs'][OPIGNO_SCORM_MANIFEST_CHOICE_EXIT_ATTR]) === 'true';
      }
    }
  }
  return $control_modes + $defaults;
}

/**
 * Extract the manifest SCO resources.
 *
 * We only extract resource information that is relevant to us. We don't care about
 * references files, dependencies, etc. Only about the href attribute, type and
 * identifier.
 *
 * @param array $manifest
 *
 * @return array
 */
function opigno_scorm_extract_manifest_resources($manifest) {
  $items = array();
  foreach ($manifest['children'] as $child) {
    if ($child['name'] == OPIGNO_SCORM_MANIFEST_RESOURCES) {
      foreach ($child['children'] as $resource) {
        if ($resource['name'] == OPIGNO_SCORM_MANIFEST_RESOURCE) {
          if (!empty($resource['attrs'][OPIGNO_SCORM_MANIFEST_ID_ATTR])) {
            $identifier = $resource['attrs'][OPIGNO_SCORM_MANIFEST_ID_ATTR];
          }
          else {
            $identifier = uniqid();
          }
          if (!empty($resource['attrs'][OPIGNO_SCORM_MANIFEST_HREF_ATTR])) {
            $href = $resource['attrs'][OPIGNO_SCORM_MANIFEST_HREF_ATTR];
          }
          else {
            $href = '';
          }
          if (!empty($resource['attrs'][OPIGNO_SCORM_MANIFEST_TYPE_ATTR])) {
            $type = $resource['attrs'][OPIGNO_SCORM_MANIFEST_TYPE_ATTR];
          }
          else {
            $type = '';
          }
          if (!empty($resource['attrs'][OPIGNO_SCORM_MANIFEST_SCORM_TYPE_ATTR])) {
            $scorm_type = $resource['attrs'][OPIGNO_SCORM_MANIFEST_SCORM_TYPE_ATTR];
          }
          else {
            $scorm_type = '';
          }
          $items[] = array(
            'identifier' => $identifier,
            'href' => $href,
            'type' => $type,
            'scorm_type' => $scorm_type,
          );
        }
      }
    }
  }
  return $items;
}

/**
 * Combine resources and SCO data.
 *
 * Update SCO data to include resource information (if necessary). Return the updated
 * SCO list.
 *
 * @param array $scos
 * @param array $resources
 *
 * @return array
 */
function opigno_scorm_combine_manifest_sco_and_resources($scos, $resources) {
  if (!empty($scos)) {
    foreach ($scos as &$sco) {

      // If the SCO has a resource identifier ("identifierref"),
      // we need to combine them.
      if (!empty($sco['resource_identifier'])) {

        // Check all resources, and break when the correct one is found.
        foreach ($resources as $resource) {
          if (!empty($resource['identifier']) && $resource['identifier'] == $sco['resource_identifier']) {

            // If the SCO has no launch attribute, get the resource href.
            if (!empty($resource['href']) && empty($sco['launch'])) {
              $sco['launch'] = $resource['href'];
            }

            // Set the SCO type, if available.
            if (!empty($resource['type']) && empty($sco['type'])) {
              $sco['type'] = $resource['type'];
            }

            // Set the SCO scorm type, if available.
            if (!empty($resource['scorm_type']) && empty($sco['scorm_type'])) {
              $sco['scorm_type'] = $resource['scorm_type'];
            }
            break;
          }
        }
      }
    }
  }
  else {
    if (!empty($resources)) {

      // Init scos array.
      $scos = array();
      foreach ($resources as $resource) {
        $sco = array();

        // Add lunch key for the sco.
        if (!empty($resource['href'])) {
          $sco['launch'] = $resource['href'];
        }

        // Add type key for the sco.
        if (!empty($resource['type'])) {
          $sco['type'] = $resource['type'];
        }

        // Add scorm type key for the sco.
        if (!empty($resource['scorm_type'])) {
          $sco['scorm_type'] = $resource['scorm_type'];
        }

        // Add scorm type key for the sco.
        if (empty($resource['title']) && !empty($resource['identifier'])) {
          $sco['title'] = $resource['identifier'];
        }

        // Set identifier and default values.
        $sco['identifier'] = $resource['identifier'];
        $sco['parent_identifier'] = 0;
        $sco['weight'] = 0;

        // Add created sco to scos list.
        $scos[] = $sco;
      }
    }
  }
  return $scos;
}

Functions

Namesort descending Description
opigno_scorm_combine_manifest_sco_and_resources Combine resources and SCO data.
opigno_scorm_extract Extract the SCORM package from the file.
opigno_scorm_extract_item_sequencing_control_modes Extract the manifest SCO item sequencing control modes.
opigno_scorm_extract_item_sequencing_objectives Extract the manifest SCO item sequencing objective.
opigno_scorm_extract_manifest_data Extract the manifest data.
opigno_scorm_extract_manifest_metadata Extract the manifest metadata.
opigno_scorm_extract_manifest_resources Extract the manifest SCO resources.
opigno_scorm_extract_manifest_scos Extract the manifest SCO items.
_opigno_scorm_extract_manifest_scos_items Helper function to recursively extract the manifest SCO items.

Constants