<?php /*

 ocPortal
 Copyright (c) ocProducts, 2004-2010

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license		http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright	ocProducts Ltd
 * @package		workflows
 */

/**
 * Adds the specified content (image, video, gallery, etc.) to the
 * specified workflow.
 *
 * @param  SHORT_TEXT		The content-meta-aware name that applies to this content.
 * @param  SHORT_TEXT		The ID of this content. Must be a string. Integers will be extracted from the string if needed.
 * @param  ?AUTO_LINK		The translation table ID of the desired workflow. -1 is none, defaults to NULL (NULL: system default)
 * @param  boolean			Whether to remove any existing workflows from this content beforehand (current permissions must allow this)
 * @return ?AUTO_LINK		The content's ID in the workflow_content table. (NULL: if not added (eg. told to use default when there isn't one))
 */
function add_content_to_workflow($content_type='', $content_id='', $workflow_id=NULL, $remove_existing=false)
{
	//TODO: Remove existing entries, however that requires permissions
	// Garbage in requires garbage out
	if ($content_type=='' || $content_id=='')
	{
		// Bail out
		fatal_exit(do_lang_tempcode('_MISSING_RESOURCE', get_translated_text($workflow_id)));
	}

	// Have we been given a valid workflow to use?
	if (!is_null($workflow_id))
	{
		$valid_workflow = false;		// The caller is guilty of providing false details until proven innocent
		// Look through the distict workflows...
		foreach ($GLOBALS['SITE_DB']->query('SELECT DISTINCT workflow_name FROM '.get_table_prefix().'workflow_requirements') as $a_workflow)
		{
			// ...and see if this one is us
			if ($a_workflow['workflow_name']==$workflow_id)
			{
				$valid_workflow=true;
			}
		}
		// Bail out if we couldn't find the specified workflow
		if (!$valid_workflow)
		{
			warn_exit(do_lang_tempcode('_MISSING_RESOURCE', get_translated_text($workflow_id)));
		}
		// Don't need this anymore
		unset($valid_workflow);
	}
	// Otherwise use the system default
	else
	{
		$default_workflow = get_default_workflow();
		if (is_null($default_workflow))
		{
			// No default, so don't apply any
			return NULL;
		}
		else
		{
			$workflow_id = $default_workflow;
		}
	}

	// Now we know the workflow is OK, what about the content?
	// Look through the content-meta-aware hooks to find it.
	// We don't need to worry too much about multiple hooks with this
	// name, since we're calling the filesystem and if there are multiple
	// files with the same path then we've got bigger problems to worry
	// about!
	$hooks=find_all_hooks('systems','content_meta_aware');
	$found=false;		// Guilty until proven innocent again
	foreach (array_keys($hooks) as $hook)
	{
		// Skip if this is not the hook we're after
		if ($hook != $content_type) continue;

		// Otherwise load and instantiate the hook
		require_code('hooks/systems/content_meta_aware/'.filter_naughty_harsh($hook));
		$ob=object_factory('Hook_content_meta_aware_'.filter_naughty_harsh($hook),true);
		if (is_null($ob)) continue;		// Bail out if the hook fails

		// Grab information about the hook
		$info=$ob->info();
		$content_table = $info['table'];
		$content_field = $info['id_field'];
		if ($info['id_field_numeric'])		// See if we need to extract a number from the provided ID string
		{
			// If so then flag it with a shorter name, and use a different
			// name for the converted ID (ocPortal avoids dynamic typing)
			$numeric = true;
			$numeric_id = intval($content_id);		// Errors arise from passing an object, but should be noticed by type checker
		}
		else
		{
			// Otherwise set the flag as false. We've already got a string.
			$numeric = false;
		}
	}

	// Now we have the information required to access the content.
	// However, we still don't know if the provided ID is valid, so we
	// have to check that too!
	// Need different paths based on ID type, to prevent breaking strict
	// databases
	if ($numeric)
	{
		// Query the database for content matching the found parameters
		if ($GLOBALS['SITE_DB']->query_select($content_table,array($content_field),array($content_field=>$numeric_id),'',1)==array())
		{
			// This content doesn't exist, bail out
			warn_exit(do_lang_tempcode('_MISSING_RESOURCE', escape_html($content_table.'/'.$content_field.'/'.$content_id)));
		}
	}
	else
	{
		// Query the database for content matching the found parameters
		if ($GLOBALS['SITE_DB']->query_select($content_table,array($content_field),array($content_field=>$content_id),'',1)==array())
		{
			// This content doesn't exist, bail out
			warn_exit(do_lang_tempcode('_MISSING_RESOURCE', get_translated_text($workflow_id)));
		}
	}

	// If we've made it this far then we have been asked to apply a
	// valid workflow to a valid piece of content, so let's go ahead

	// Remove existing associations if we've been asked to
	if ($remove_existing)
	{
		$wf = get_workflow_of_content($content_type, $content_id);
		if (!is_null($wf))
		{
			$workflow_content_id = array_map('current',$GLOBALS['SITE_DB']->query_select('workflow_content',array('id'),array('source_type'=>$content_type, 'source_id'=>$content_id)));
			foreach ($workflow_content_id as $this_id)
			{
				$GLOBALS['SITE_DB']->query_delete('workflow_content',array('id'=>$this_id));
				$GLOBALS['SITE_DB']->query_delete('workflow_content_status',array('workflow_content_id'=>$this_id));
			}
		}
	}
	$id = $GLOBALS['SITE_DB']->query_insert('workflow_content',array('source_type'=>$content_type,'source_id'=>$content_id,'workflow_name'=>$workflow_id,'notes'=>'','original_submitter'=>get_member()),true,false,false);

	// Set the workflow status to 0 for each point
	foreach (get_requirements_for_workflow($workflow_id) as $point)
	{
		$GLOBALS['SITE_DB']->query_insert('workflow_content_status',array('workflow_content_id'=>$id,'workflow_approval_name'=>$point,'status_code'=>0,'approved_by'=>get_member()),true,false,false);
	}

	return $id;
}

/**
 * Give the specified workflow a new requirement. If you need to invent
 * a new requirement that's not been used before then just add its name
 * to the translation table via the 'insert_lang' function and use the
 * returned ID as that requirement's ID from then on.
 * Note: The workflow must already exist with at least one requirement.
 * See the 'build_new_workflow' function to define a new workflow.
 * Note: If passing a requirement already in the given workflow, its
 * position will be updated to the new value (or the end if not specified).
 * NOTE: Positions do not need to be contiguous, as long as they are in
 * an order. For example, requirements at positions 12, 34 and 852 will
 * behave the same way as requirements at positions 1, 2 and 3 (except
 * when adding requirements at defined positions of course, since
 * position 4 will appear at the start of the former list but at the end
 * of the latter)
 *
 * @param  AUTO_LINK		The ID of the requirement
 * @param  AUTO_LINK		The ID of the workflow to add this requirement to
 * @param  ?integer		The position in the workflow that this requirement will have. NULL adds it to the end (NULL: default)
 */
function add_requirement_to_workflow($requirement_id,$workflow_id,$position=NULL)
{
	// Check we've not been given garbage. If so then bail out.
	if (is_null($requirement_id))
	{	
		fatal_exit(do_lang_tempcode('_MISSING_RESOURCE', 'NULL requirement'));
	}
	if (is_null($workflow_id))
	{
		fatal_exit(do_lang_tempcode('_MISSING_RESOURCE', 'NULL workflow'));
	}

	// Make sure this workflow exists
	if ($GLOBALS['SITE_DB']->query_select('workflow_requirements',array('workflow_name'),array('workflow_name'=>$workflow_id),'',1)==array())
	{
		// If not then give a warning and exit
		warn_exit(do_lang_tempcode('_MISSING_RESOURCE', get_translated_text($workflow_id)));
	}

	// If this requirement is already in this workflow then we need to
	// update its position. However, it is easiest if we simply delete
	// the entry here, so that the following code will apply to all cases
	$GLOBALS['SITE_DB']->query_delete('workflow_requirements',array('workflow_name'=>$workflow_id,'workflow_approval_name'=>$requirement_id),'',1,NULL,true);

	// Now see what position we're adding to. We either need to determine
	// the next position (if we've been given NULL), or else just dump
	// the position in the record (we don't assume that positions are
	// unique in any case, points with the same position will appear
	// together but their specific order is undefined)
	if (is_null($position))
	{
		// The easy case, we simply grab the existing requirements in
		// order of position and +1 to the highest.
		$current_positions = $GLOBALS['SITE_DB']->query_select('workflow_requirements',array('the_position'),array('workflow_name'=>$workflow_id),'ORDER BY the_position DESC',1);
		$position = $current_positions[0]['the_position'] + 1;
	}

	// Do the insertion.
	// TODO: We should set the default appropriately here, since the case
	// of adding new requirements then removing the original requirements
	// would cause a workflow to lose its default status.
	$GLOBALS['SITE_DB']->query_insert('workflow_requirements',array('workflow_name'=>$workflow_id,'workflow_approval_name'=>$requirement_id,'the_position'=>$position,'is_default'=>0),true,false,false);
}

/**
 * Approves the given point for the given piece of content. The optional
 * user argument will be set as the approver (ie. the one to blame if
 * this approval is in error) , otherwise the current user will be set.
 *
 * @param  AUTO_LINK		The *workflow content* ID (NOT the gallery, category, etc. ID!)
 * @param  AUTO_LINK		The approval point name (translation table ID)
 * @param  ?MEMBER		The user ID to use as the approver, (NULL: for current user)
 */
function approve_content_for_point($content_id,$approval_name,$user=NULL)
{
	// TODO: Add some sanity checks here
	// Grab the current user if we've not been given one
	if (is_null($user)) $user = get_member();

	// Grab the current status of this point, firstly to see if we need
	// to set it at all, but more importantly to make sure that the given
	// parameters make sense and are valid
	$status = $GLOBALS['SITE_DB']->query_select('workflow_content_status',array('status_code'),array('workflow_content_id'=>$content_id,'workflow_approval_name'=>$approval_name),'',1);

	// Now approve the point if needed
	if ($status['status_code'] == 0)
	{
		$GLOBALS['SITE_DB']->query_update('workflow_content_status',array('status_code'=>1),array('workflow_content_id'=>$content_id,'workflow_approval_name'=>$approval_name),'',1);
	}

	// Done
}

/**
 * Add a new workflow to the system. The workflow needs at least one
 * requirement, which you should put in the $requirements array passed
 * as the second argument. The indices of this array must be natural
 * numbers, since they will indicate the position of this requirement in
 * the workflow. The requirements themselves are simply integers which
 * map to language strings containing the requirement name. They can be
 * made up arbitrarily before passing to this function by calling
 * insert_lang('requirement_name').
 *
 * @param  ?AUTO_LINK	The ID of this workflow, if you've already inserted the language string. (NULL: If not already inserted).
 * @param  string			The string to use as this requirement's name in the current language
 * @param  array			A mapping from workflow position to workflow requirement (language string lookup integer)
 * @param  boolean		Whether to make this workflow the default workflow (default value is false)
 * @return AUTO_LINK		The workflow's ID (language string lookup integer)
 */
function build_new_workflow($id, $name, $requirements, $default=false)
{
	// First let's get a unique ID for this workflow if it has none
	if (is_null($id))
	{
		// HACKHACK We should use a normalised table with an ID field
		$id = insert_lang($name,3);
	}
	// Remove any existing requirements just in case this workflow already exists
	$GLOBALS['SITE_DB']->query_delete('workflow_requirements',array('workflow_name'=>$id));

	// Now add each requirement to this workflow; as a side-effect this will
	// give the workflow a presence in the database
	foreach ($requirements as $position=>$requirement)
	{
		// Set default to 0 to start with
		$GLOBALS['SITE_DB']->query_insert('workflow_requirements',array('workflow_name'=>$id,'workflow_approval_name'=>$requirement,'the_position'=>$position,'is_default'=>0),true,false,false);
	}
	// Now see if we're to be the default
	if ($default)
	{
		// If so then set all defaults to 0 (which is why we didn't bother
		// setting it upon insertion)
		$GLOBALS['SITE_DB']->query_update('workflow_requirements',array('is_default'=>0));
		// Now set our defaults to 1
		$GLOBALS['SITE_DB']->query_update('workflow_requirements',array('is_default'=>1),array('workflow_name'=>$id));
	}
	// And we're done!
	return $id;
}

/**
 * Deleting a workflow will remove the workflow, leaving the validated/
 * unvalidated system to handle content, ie. content which has passed
 * completely through the workflow will have its validated bit set and
 * will thus remain live. Those not completely through will not have
 * theirs set yet and will thus remain unvalidated and not live.
 * NOTE: Approval points can be reused, so they will stay behind.
 *
 * @param  AUTO_LINK		The ID of the workflow to delete
 */
function delete_workflow($id)
{
	// Grab all of the content in this workflow
	$content = $GLOBALS['SITE_DB']->query_select('workflow_content',array('id','source_type','source_id'),array('workflow_name'=>$id));

	// Now remove those references
	$GLOBALS['SITE_DB']->query_delete('workflow_content',array('workflow_name'=>$id));

	// Then remove their workflow status
	foreach ($content as $content_item)
	{
		$GLOBALS['SITE_DB']->query_delete('workflow_content_status',array('workflow_content_id'=>$content_item['id']));
	}

	// Grab the approval points in this workflow and remove those which aren't
	// used by any other workflows
	$points = array_map('current',$GLOBALS['SITE_DB']->query_select('workflow_requirements',array('workflow_approval_name'),array('workflow_name'=>$id)));
	foreach ($points as $p)
	{
		if (count(
			array_unique(
				array_map('current',$GLOBALS['SITE_DB']->query_select('workflow_requirements',array('workflow_name'),array('workflow_approval_name'=>$p)))
			)
		) == 1)
		{
			// This approval point is only in one workflow (ie. this one) so remove
			// it from the permissions table
			$GLOBALS['SITE_DB']->query_delete('workflow_permissions',array('workflow_approval_name'=>$p));
		}
	}

	// Now remove the workflow from the database (ie. remove its association with
	// approval points)
	$GLOBALS['SITE_DB']->query_delete('workflow_requirements',array('workflow_name'=>$id));
}

/**
 * Deleting an approval point will remove it from any workflow it is a
 * part of. Any content which has been approved for this requirement
 * will be unaffected, whilst those not-yet-approved will first be
 * approved, then have the requirement removed. This is to prevent any
 * content asking to be approved on a point which doesn't exist.
 *
 * @param  AUTO_LINK		The workflow approval point name (translation table ID)
 */
function delete_approval_point($name)
{
	// Grab all content awaiting this approval
	$content = $GLOBALS['SITE_DB']->query_select('workflow_content_status', array('status_code','workflow_content_id'),array('workflow_approval_name'=>$name));

	// Now go through each, approving them if needed
	foreach ($content as $content_item)
	{
		// 0 means unapproved
		if ($content['status_code'] == 0)
		{
			// Set the approval
			approve_content_for_point($content['workflow_content_id'], $name);
		}
	}
	// Now remove these requirements en-mass from the content
	$GLOBALS['SITE_DB']->query_delete('workflow_content_status',array('workflow_content_id'=>$name));

	// We have to be careful about removing requirements from workflows,
	// since a workflow is defined by the approval points it requires.
	// We must check to see if we're about to throw out any workflows as
	// a result of removing this point. If so then we'd like to remove
	// the workflow sanely and completely.
	$affected_workflows = $GLOBALS['SITE_DB']->query_select('workflow_requirements', array('workflow_name'), array('workflow_approval_name'=>$name));
	foreach ($affected_workflows as $workflow)
	{
		// If there is only one requirement then it's the one we've been
		// asked to remove. Let's just remove the whole workflow.
		if (count(get_requirements_for_workflow($workflow['workflow_name'])) == 1)
		{
			delete_workflow($workflow['workflow_name']);
		}
		// Otherwise we can just remove this one point
		else
		{
			$GLOBALS['SITE_DB']->query_delete('workflow_requirements',array('workflow_approval_name'=>$name, 'workflow_name'=>$workflow['workflow_name']),'',1);
		}
	}

	// Now we remove the permissions associated with this approval point
	$GLOBALS['SITE_DB']->query_delete('workflow_permissions', array('workflow_approval_name'=>$name));
}

/**
 * Returns all of the workflows which are currently defined.
 *
 * @return array		The workflows which are defined. Empty if none are defined.
 */
function get_all_workflows()
{
	$workflows = array_map('current',$GLOBALS['SITE_DB']->query('SELECT DISTINCT workflow_name FROM '.get_table_prefix().'workflow_requirements'));
	$output = array();
	foreach ($workflows as $w)
	{
		$output[$w] = get_translated_text($w);
	}
	return $output;
}

/**
 * Returns all of the approval point which are currently defined. Indices are
 * IDs, values are names.
 *
 * @return array		The approval points which are defined. Empty if none are defined.
 */
function get_all_approval_points()
{
	$points = array_map('current',$GLOBALS['SITE_DB']->query('SELECT DISTINCT workflow_approval_name FROM '.get_table_prefix().'workflow_requirements'));
	$points = array_unique(array_merge($points,array_map('current',$GLOBALS['SITE_DB']->query('SELECT DISTINCT workflow_approval_name FROM '.get_table_prefix().'workflow_permissions'))));
	$output = array();
	foreach ($points as $p)
	{
		$output[$p] = get_translated_text($p);
	}
	return $output;
}

/**
 * Get the system's default workflow. If there is only one workflow this
 * will return it, otherwise (multiple with no default specified, or no
 * workflows at all) it will give NULL.
 *
 * @return ?AUTO_LINK		The ID of the default workflow. (NULL: if none set)
 */
function get_default_workflow()
{
	// Grab every workflow ID
	$workflows = get_all_workflows();
	// Only bother doing anything more complicated if we've got multiple
	// workflows to dig through
	if (count($workflows)>1)
	{
		// Look for those which are set as default
		$defaults = $GLOBALS['SITE_DB']->query('SELECT DISTINCT workflow_name FROM '.get_table_prefix().'workflow_requirements WHERE is_default=1');
		// If there aren't any then we can't presume to know which should
		// be returned, so return an empty array
		if ($defaults==array())
		{
			return NULL;
		}
		// Likewise we cannot choose between multiple defaults, so return
		// an empty array
		elseif (count($defaults) > 1)
		{
			return NULL;
		}
		// If we're here then we have one default, so return it
		return current(current($defaults));
	}
	// Otherwise just give back what we've found (singleton or empty)
	elseif (count($workflows) == 1)
	{
		return current(array_keys($workflows));
	}
	else
	{
		return NULL;
	}
}

/**
 * Get the workflow content ID for the given piece of content.
 *
 * @param  string		The type of the source (eg. download, gallery, etc.)
 * @param  string		The ID of the specific piece of content (if numeric, pass as a string anyway)
 * @return AUTO_LINK	The workflow_content_id
 */
function get_workflow_content_id($source_type,$source_id)
{
	// Grab the specified content's ID
	$content = $GLOBALS['SITE_DB']->query_select('workflow_content',array('id'),array('source_type'=>$source_type,'source_id'=>$source_id),'',1);
	if ($content == array())
	{
		return NULL;
	}
	return $content[0]['id'];
}

/**
 * Gets an array of the approval point IDs required by the given
 * workflow ID
 *
 * @param  AUTO_LINK		The ID of the workflow
 * @return array			The IDs of the approval points for the workflow, in workflow order
 */
function get_requirements_for_workflow($workflow_id)
{
	// Make sure we have something to work with
	if (is_null($workflow_id))
	{
		warn_exit(do_lang_tempcode('_MISSING_RESOURCE', 'NULL workflow'));
	}
	// Now grab each point along with its position
	$approval_points = $GLOBALS['SITE_DB']->query_select('workflow_requirements',array('workflow_approval_name'),array('workflow_name'=>$workflow_id),'ORDER BY the_position ASC');
	$raw_names = array();		// This will store the points in order
	// Fill the $raw_names array
	foreach ($approval_points as $point)
	{
		$raw_names[] = $point['workflow_approval_name'];
	}
	return $raw_names;
}

/**
 * Gets the position of the given requirement in the given workflow.
 *
 * @param  AUTO_LINK		The ID of the approval point
 * @param  AUTO_LINK		The ID of the workflow
 * @return ?integer		The position of the approval point in this case (NULL: if not found)
 */
function get_requirement_position($requirement_id, $workflow_id)
{
	$found = $GLOBALS['SITE_DB']->query_select('workflow_requirements',array('the_position'),array('workflow_name'=>$workflow_id,'workflow_approval_name'=>$requirement_id),'ORDER BY the_position ASC');
	if ($found == array())
	{
		return NULL;
	}
	else
	{
		return $found[0]['the_position'];
	}
}

/**
 * Gets an array of the group IDs allowed to approve the given point
 *
 * @param  AUTO_LINK		The ID of the approval point
 * @param  boolean		Should we only return groups which are validated? (default: true)
 * @return array			The IDs of the groups allowed to signoff on it
 */
function get_groups_for_point($approval_id,$only_validated=true)
{
	if (is_null($approval_id))
	{
		warn_exit(do_lang_tempcode('_MISSING_RESOURCE', 'NULL approval'));
	}
	$groups = $GLOBALS['SITE_DB']->query_select('workflow_permissions',array('usergroup'),array('workflow_approval_name'=>$approval_id));
	$raw_names = array();
	foreach ($groups as $group)
	{
		$raw_names[] = $group['usergroup'];
	}
	return $raw_names;
}

/**
 * Find out who submitted a piece of content from a workflow.
 *
 * @param  AUTO_LINK		The workflow content ID
 * @return ?MEMBER		The submitter (NULL: if unknown)
 */
function get_submitter_of_workflow_content($content_id)
{
	// Exit on misuse
	if (is_null($content_id))
	{
		warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
	}
	// Find out the author straight from the workflow_content table
	$submitter = $GLOBALS['SITE_DB']->query_select('workflow_content',array('original_submitter'),array('id'=>$content_id));
	if ($submitter == array())
	{
		// Exit if we can't find the given resource
		warn_exit(do_lang_tempcode('_MISSING_RESOURCE','workflow_content->'.strval($content_id)));
	}
	// Now extract the submitter (if there is one)
	return $submitter[0]['original_submitter'];
}

/**
 * Get the tempcode for viewing/editing the workflow status of the given content
 *
 * @param  AUTO_LINK	The ID of this content in the workflow_content table
 * @return tempcode	The form for this content
 */
function get_workflow_form($workflow_content_id)
{
	// Load our prerequisites
	require_lang('workflows');
	require_code('form_templates');
	require_code('users');

	// Do not let guests edit the status of content
	if (is_guest())
	{
		return new ocp_tempcode();
	}

	//////////////////////////////////////////////////
	// Gather the details we need to build the form //
	//////////////////////////////////////////////////

	// These will hold the form code
	$workflow_form = new ocp_tempcode();
	$workflow_fields = new ocp_tempcode();
	$workflow_hidden = new ocp_tempcode();

	// We already know the content ID
	$workflow_hidden->attach(form_input_hidden('content_id',strval($workflow_content_id)));

	// Check if this is a valid piece of content for a workflow
	$row = $GLOBALS['SITE_DB']->query_select('workflow_content',array('*'),array('id'=>$workflow_content_id),'',1);
	if (count($row) == 0)
	{
		warn_exit(do_lang_tempcode('_MISSING_RESOURCE', get_translated_text($workflow_content_id)));
	}

	$row = array_pop($row);

	// If so then find the workflow name for it
	$relevant_workflow = $row['workflow_name'];

	$workflow_hidden->attach(form_input_hidden('workflow_id',strval($relevant_workflow)));

	// Make sure there are some points to approve
	$approval_points = get_requirements_for_workflow($relevant_workflow);
	if ($approval_points == array())
	{
		warn_exit(do_lang_tempcode('_MISSING_RESOURCE', get_translated_text($workflow_content_id)));
	}

	/////////////////////////
	// Approval tick boxes //
	/////////////////////////

	$available_groups = $GLOBALS['FORUM_DRIVER']->get_members_groups(get_member());		// What groups our user is in
	$existing_status = array();		// This shows the current approval status, but is not editable
	$approval_status = array();		// This holds tick-boxes for editing the approval status
	$send_next = array();		// This holds the details for who to send this to next
	$approve_count = 0;		// This keeps each approval box distinct
	$send_count = 0;		// This keeps the 'send next' boxes distinct
	$groups_shown = array();		// This keeps track of the groups we've already shown
	$group_names = $GLOBALS['FORUM_DRIVER']->get_usergroup_list();		// Allows us to find readable group names
	$permission_limit = false;		// Points are added in order, so take notice when we no longer have permission (so we can send to whoever does)
	$next_point = NULL;		// This will store the next workflow point for which we don't have the permission to approve

	// Get the status of each point
	$statuses = array();
	foreach ($GLOBALS['SITE_DB']->query_select('workflow_content_status',array('workflow_approval_name', 'status_code'),array('workflow_content_id'=>$workflow_content_id)) as $stat)
	{
		$statuses[$stat['workflow_approval_name']] = $stat['status_code'];
	}

	// Assume we can't do anything
	$have_permission_over_a_point = false;

	// Now loop through all approval points in workflow order	
	foreach ($approval_points as $point)
	{
		// Only show one tick box for each approval point, if any
		$approval_shown = false;

		// Go through each group allowed to modify this value...
		foreach (get_groups_for_point($point) as $allowed_group)
		{
			// ... and check whether the user is in it
			if (!$approval_shown && in_array($allowed_group, $available_groups))
			{
				// If so then remember that we've handled this point
				$approval_shown = true;

				// Remember that we can tick something
				$have_permission_over_a_point = true;

				// Add the details to the editable tick-box values
				$approval_status[$approve_count] = array();
				$approval_status[$approve_count][] = get_translated_text($point);		// Pretty name
				$approval_status[$approve_count][] = 'approval_'.strval($point);		// Name
				$approval_status[$approve_count][] = array_key_exists($point,$statuses)? $statuses[$point] : 0;		// The value (defaults to 0)
				$approval_status[$approve_count][] = do_lang_tempcode('APPROVAL_TICK_DESCRIPTION',get_translated_text($point));		// Description
				$approval_status[$approve_count][] = false;		// Not disabled, since we have permission

				// Add the details to the uneditable, existing status values
				$existing_status[$approve_count] = array();
				$existing_status[$approve_count][] = get_translated_text($point);		// Pretty name
				$existing_status[$approve_count][] = 'existing_'.strval($point);		// Name
				if (array_key_exists($point,$statuses) && ($statuses[$point]==1))
				{
					$existing_status[$approve_count][] = 1;		// The value (1 due to our if condition)
					$existing_status[$approve_count][] = do_lang_tempcode('ALREADY_APPROVED',get_translated_text($point));		// Description
				}
				else
				{
					$existing_status[$approve_count][] = 0;		// The value defaults to 0
					$existing_status[$approve_count][] = do_lang_tempcode('NOT_YET_APPROVED',get_translated_text($point));		// Description
				}
				$existing_status[$approve_count][] = true;		// Disabled, since this is for informative purposes only

				// Increment the unique ID for the array elements
				$approve_count = $approve_count + 1;
			}

			// We want the send boxes to name usergroups, rather than
			// approval points, so we may as well add those here.
			// Take care not to add groups we've already got!
			if (!in_array($allowed_group,$groups_shown))
			{
				$send_next[$allowed_group] = array();
				$send_next[$allowed_group][] = $group_names[$allowed_group];		// Pretty name
				$send_next[$allowed_group][] = 'send_'.strval($allowed_group);		// Name
				// Set the default value. We want groups allowed to approve the
				// next+1 point ticked (assuming we're approving the next one)
				// For simplicity, let's keep these unticked for now.
				//if (in_array($allowed_group['usergroup'],$groups_shown))
				//{
					$send_next[$allowed_group][] = false;
				//}
				$send_next[$allowed_group][] = do_lang_tempcode('NEXT_APPROVAL_DESCRIPTION',$group_names[$allowed_group]);		// Description
				$groups_shown[] = $allowed_group;
			}
		}

		// If it's not handled at this point, we don't have permission
		if (!$approval_shown)
		{
			// Thus we should show a disabled check box
			$existing_status[$approve_count] = array();
			$existing_status[$approve_count][] = get_translated_text($point);		// Pretty name
			$existing_status[$approve_count][] = 'approval_'.strval($point);		// Name
			$existing_status[$approve_count][] = array_key_exists($point,$statuses)? $statuses[$point] : 0;		// Value (defaults to 0)
			$existing_status[$approve_count][] = do_lang_tempcode('APPROVAL_TICK_DESCRIPTION',get_translated_text($point));		// Description
			$existing_status[$approve_count][] = true;		// Disabled, we have no permission
			// Increment the unique ID for the array elements
			$approve_count = $approve_count + 1;

			// If this is the first entry we're not permitted to change and
			// it is not already ticked...
			if (!$permission_limit && (!array_key_exists($point,$statuses) || ($statuses[$point]==0)))
			{
				// ... remember it (since we can send-to those who are
				// permitted)
				$next_point = $point;
				$permission_limit = true;
			}
		}

	}

	// If we have no control over this workflow then don't bother showing it
	if (!$have_permission_over_a_point)
	{
		return new ocp_tempcode();
	}

	///////////////////
	// Send-to boxes //
	///////////////////

	// Now we attempt to impose some sort of order to the send-to boxes.
	// The order we're going to use is to collect groups where the sum of
	// the workflow positions they can approve is equal, and put the
	// collections in ascending order. The collections are ordered based
	// on the group's average workflow approval position. Any further
	// ambiguity is not worth considering.
	$group_scores = array();		// Track the sum of positions for each group
	$group_counts = array();		// Track the number of points each group can approve (so we can calculate the average)
	$active_groups = array();		// Note which groups have permission to approve the next workflow point we can't do ourselves
	// Get all of the approval points and all of the groups
	foreach ($approval_points as $point)
	{
		foreach (get_groups_for_point($point) as $group)
		{
			// See whether this group should be active
			if ($next_point==$point)
			{
				$active_groups[] = $group;
			}
			// Add on the point's position to this group's score
			if (array_key_exists($group, $group_scores))
			{
				$group_scores[$group] = $group_scores[$group] + get_requirement_position($point, $relevant_workflow);
			}
			// Otherwise give it a new score equal to this point's position
			else
			{
				$group_scores[$group] = get_requirement_position($point, $relevant_workflow);
			}
			// Now increment the group's approval point count
			if (array_key_exists($group, $group_counts))
			{
				$group_counts[$group] = $group_counts[$group] + 1;
			}
			else
			{
				$group_counts[$group] = 1;
			}
		}
	}
	// Now we can order the groups:
	$group_order = array();		// This will store collections of groups in order of score
	foreach ($group_scores as $group_id=>$score)
	{
		// Make a new collection if we've not got one
		if (!array_key_exists($score,$group_order))
		{
			$group_order[$score] = array();
		}
		// Add this group to the collection
		$group_order[$score][] = $group_id;
	}

	// Finally we can get the tick-box details we found earlier and put
	// them in the right order
	$send_to_boxes = array();
	foreach ($group_order as $score=>$collection)
	{
		// Add the group if its the only one
		if (count($collection)==1)
		{
			// Before adding the tick box, see if it should be active
			if (in_array($collection[0],$active_groups))
			{
				$send_next[$collection[0]][2] = 1;
			}
			$send_to_boxes[] = $send_next[$collection[0]];
		}
		else
		{
			// Otherwise order the collection first
			$collection_order = array();
			foreach ($collection as $group_id)
			{
				// Work out the average position of each group's approval
				// points (we need an int for use as an index, but add a
				// couple of orders of magnitude to give us 2 more sig figs)
				$group_average = intval((floatval($score) / floatval($group_counts[$group_id]))*100);
				// Make a new collection if we've not got one
				if (!array_key_exists($group_average,$collection_order))
				{
					$collection_order[$group_average] = array();
				}
				// Add this group to the average collection
				$collection_order[$group_average][] = $group_id;
			}
			// Now we can add them in order of average
			foreach ($collection_order as $average_collection)
			{
				foreach ($average_collection as $group_id)
				{
					// Before adding the tick box, see if it should be active
					if (in_array($group_id,$active_groups))
					{
						$send_next[$group_id][2] = 1;
					}
					$send_to_boxes[] = $send_next[$group_id];
				}
			}
		}
	}

	// Now tack the original submitter onto the end of the send-to list,
	// if we know who it is
	$submitter = get_submitter_of_workflow_content($workflow_content_id);
	if (!is_null($submitter))
	{
		$submitter_details = array();		// Build the array independently
		$submitter_details[] = $GLOBALS['FORUM_DRIVER']->get_username($submitter).' ('.do_lang('SUBMITTER').')';
		$submitter_details[] = 'send_author';		// Name
		$submitter_details[] = false;		// Value
		$submitter_details[] = do_lang_tempcode('NEXT_APPROVAL_AUTHOR',$GLOBALS['FORUM_DRIVER']->get_username($submitter));		// Description
		$send_to_boxes[] = $submitter_details;		// Then tack it on the end
	}

	/* NOTE: Not currently used; content becomes live once all points are
	 * approved
	///////////////////
	// Live tick-box //
	///////////////////
	// The 'Live' tickbox is just the regular 'validated' box with a thin
	// veneer of approval logic. It should only be enabled if we have
	// permission over all approval points (ie. there is no 'next' point)
	if (is_null($next_point))
	{
		$live_box = form_input_tick(do_lang_tempcode('WORKFLOW_LIVE'),do_lang_tempcode('WORKFLOW_LIVE_DESCRIPTION'),'validated',$validated,NULL,'1',false);
	}
	else
	{
		$live_box = form_input_tick(do_lang_tempcode('WORKFLOW_LIVE'),do_lang_tempcode('WORKFLOW_LIVE_DESCRIPTION'),'validated',$validated,NULL,'1',true);
	}			
	*/

	////////////////////////
	// Now build the form //
	////////////////////////

	// Bail out if there's nothing to the workflow
	if ($approval_status==array())
	{
		return new ocp_tempcode();
	}

	// Attach the title to the form first, along with usage info
	$workflow_fields->attach(do_template('FORM_SCREEN_FIELD_SPACER',array('TITLE'=>NULL,'HELP'=>do_lang_tempcode('WORKFLOW_USAGE'))));

	// Show the current status next
	$workflow_fields->attach(form_input_various_ticks($existing_status,'',NULL,do_lang_tempcode('CURRENT_APPROVAL_STATUS'),false));

	// Attach the workflow tick boxes next
	$workflow_fields->attach(form_input_various_ticks($approval_status,'',NULL,do_lang_tempcode('POINTS_TO_APPROVE'),false));

	// Add the 'live' tickbox
	// NOTE: Not used at the moment; content becomes live once all points
	// have been approved
	//$workflow_fields->attach($live_box);

	// Add a section for notes
	$notes = $GLOBALS['SITE_DB']->query_select('workflow_content',array('notes'),array('id'=>$workflow_content_id));
	$workflow_fields->attach(form_input_huge(do_lang('NOTES'),do_lang('WORKFLOW_NOTES_DESCRIPTION'),'workflow_notes',$notes[0]['notes'],false,NULL,6,'',true));

	// Set who to send it to next
	$workflow_fields->attach(form_input_various_ticks($send_to_boxes,do_lang_tempcode('NEXT_APPROVAL_SIDE_DESCRIPTION'),NULL,do_lang_tempcode('NEXT_APPROVAL'),false));

	// Set the URL for handling the response to this form
	$post_url = build_url(array('page'=>'_SELF','type'=>'workflow'),'_SELF');

	// Set the URL to return to after the handling has taken place
	$workflow_hidden->attach(form_input_hidden('return_url',get_self_url(true)));

	// Add all of these to the form
	$workflow_form->attach(do_template('FORM',array('FIELDS'=>$workflow_fields,'HIDDEN'=>$workflow_hidden,'TEXT'=>'','URL'=>$post_url,'SUBMIT_NAME'=>do_lang_tempcode('SUBMIT_WORKFLOW_CHANGES'),'SKIP_REQUIRED'=>true)));

	// Then pass it to whoever wanted it
	return put_in_standard_box($workflow_form,do_lang_tempcode('WORKFLOW_STATUS'));
}

/**
 * Handler for workflow form submission.
 *
 * @return tempcode		Either an error page or a success message
 */
function workflow_update_handler()
{
	require_lang('workflows');
	require_code('database');
	$success_message = do_lang('APPROVAL_UNCHANGED');

	/////////////////////////////////////////
	// Grab everything we need from $_POST //
	/////////////////////////////////////////

	$workflow_id=post_param('workflow_id');
	$content_id=post_param('content_id');
	$workflow_notes=post_param('workflow_notes');

	// Find out which approvals have been given
	$approvals = array();
	foreach (get_requirements_for_workflow(post_param('workflow_id')) as $approval_id)
	{
		// We might not have a value for this point, since we may not have given a tick box for it
		if (array_key_exists('approval_'.strval($approval_id),$_POST))
		{
			$approvals[$approval_id] = post_param_integer('approval_'.strval($approval_id));
		}
		// Unticked boxes don't seem to appear in $_POST, so get the
		// raw tickbox value instead
		elseif (array_key_exists('tick_on_form__approval_'.strval($approval_id),$_POST))
		{
			$approvals[$approval_id] = post_param_integer('tick_on_form__approval_'.strval($approval_id));
		}
	}

	////////////////////////
	// Get member details //
	////////////////////////

	// Find out which groups/members to inform, starting with the
	// original submitter
	$send_to_members = array();
	if (array_key_exists('send_author',$_POST))
	{
		if (post_param_integer('send_author')==1)
		{
			$submitter = get_submitter_of_workflow_content($content_id);
			if (!is_null($submitter))
			{
				$send_to_members[$submitter] = 1;
			}
		}
	}

	// Now get the groups
	$group_ids = array();		// Only remember 1 copy of each group
	foreach (get_requirements_for_workflow($workflow_id) as $requirement)
	{
		foreach (get_groups_for_point($requirement) as $group)
		{
			if (!in_array($group, $group_ids))
			{
				if (post_param_integer('send_'.strval($group),0)==1)
					$group_ids[] = $group;
			}
		}
	}

	// From the groups we can get the members, and from there the emails
	foreach ($GLOBALS['FORUM_DRIVER']->member_group_query($group_ids) as $member)		// FIXME: OCF-specific
	{
		$send_to_members[$member['id']] = 1;
	}

	////////////////////////////////////////////
	// Now play with the database as required //
	////////////////////////////////////////////

	// See which values need updating (ie. approvals have been given/withdrawn)
	$updated_approvals = array();
	$all_approval_statuses = array();

	// Grab each point's status from the database
	$old_values = $GLOBALS['SITE_DB']->query_select('workflow_content_status',array('workflow_approval_name', 'status_code'),array('workflow_content_id'=>$content_id));

	$accounted_for_statuses = array();

	// Look for any differences we need to make
	foreach ($old_values as $old_value)
	{
		$noted = false;		// Keep a note of each value for including in emails
		// Only compare against values which we've been given
		if (array_key_exists($old_value['workflow_approval_name'],$approvals))
		{
			$accounted_for_statuses[] = $old_value['workflow_approval_name'];

			// See if the database entry is the same as the given status
			if ($old_value['status_code'] != $approvals[$old_value['workflow_approval_name']])
			{
				// If not then see if we have permission to change it
				$members_with_permission = array();
				foreach ($GLOBALS['FORUM_DRIVER']->member_group_query(get_groups_for_point($old_value['workflow_approval_name'])) as $permitted)
				{
					$members_with_permission[] = $permitted['id'];
				}
				if (in_array(get_member(), $members_with_permission))
				{
					// Remember that this needs to be changed
					$updated_approvals[$old_value['workflow_approval_name']] = $approvals[$old_value['workflow_approval_name']];
					// Make a note of this value in the array of all statuses
					$all_approval_statuses[$old_value['workflow_approval_name']] = $approvals[$old_value['workflow_approval_name']];
					$noted = true;
				}
			}
		}
		if (!$noted)
		{
			// If we're here then this status has either not been passed or
			// it does not need modifying. Either way we can grab a valid
			// status from the database.
			$all_approval_statuses[$old_value['workflow_approval_name']] = $old_value['status_code'];
		}
	}
	// Now add any unaccounted-for points to those which need updating
	$new_approvals = array();
	foreach (array_keys($approvals) as $a)
	{
		if (!in_array($a, $accounted_for_statuses))
		{
			$updated_approvals[$a] = $approvals[$a];
			$new_approvals[] = $a;
		}
	}
	// Now we know which fields to update, let's do so
	foreach ($updated_approvals as $approval_id=>$status_code)
	{
		$success_message = do_lang('APPROVAL_CHANGED_DESCRIPTION');
		if (in_array($approval_id,$new_approvals))
		{
			$GLOBALS['SITE_DB']->query_insert('workflow_content_status',array('status_code'=>$status_code,'approved_by'=>get_member(),'workflow_content_id'=>$content_id,'workflow_approval_name'=>$approval_id));
		}
		else
		{
			$GLOBALS['SITE_DB']->query_update('workflow_content_status',array('status_code'=>$status_code,'approved_by'=>get_member()),array('workflow_content_id'=>$content_id,'workflow_approval_name'=>$approval_id),'',1,NULL,false,false);
		}
	}

	// Update the notes (this is done destructively, but is simplest)
	// We append a timestamped log of the action taken
	$notes_approved = array();
	$notes_disapproved = array();
	foreach ($updated_approvals as $approval_id=>$status_code)
	{
		if ($status_code)
		{
			$notes_approved[] = $approval_id;
		}
		else
		{
			// Just because it's not approved, doesn't mean that it was unticked.
			// It may have just been added to the workflow.
			if (!in_array($approval_id,$new_approvals))
				$notes_disapproved[] = $approval_id;
		}
	}
	if (count($notes_approved)+count($notes_disapproved) > 0)
	{
		$note_title = date('Y-m-d H:i').' '.$GLOBALS['FORUM_DRIVER']->get_username(get_member());
		$workflow_notes = $workflow_notes."\n\n".$note_title."\n".str_repeat('-', strlen($note_title));

		$notes_approved = array_map('get_translated_text',$notes_approved);
		$notes_disapproved = array_map('get_translated_text',$notes_disapproved);
		if (count($notes_approved) > 0)
			$workflow_notes .= "\n".do_lang('WORKFLOW_APPROVED').': '.implode(', ',$notes_approved);
		if (count($notes_disapproved) > 0)
			$workflow_notes .= "\n".do_lang('WORKFLOW_DISAPPROVED').': '.implode(', ',$notes_disapproved);
	}
	$GLOBALS['SITE_DB']->query_update('workflow_content',array('notes'=>$workflow_notes),array('id'=>$content_id),'',1);

	/////////////////////////////
	// See if we're going live //
	/////////////////////////////

	// Validation is stored, for the most part, in a 'validated' field
	// of the content's table. Those which store it elsewhere must
	// specify this via their content-meta-aware info.

	// Grab lookup data from the workflows database
	$content_details = $GLOBALS['SITE_DB']->query_select('workflow_content',array('source_type', 'source_id'),array('id'=>$content_id),'',1);
	if ($content_details==array())
	{
		warn_exit(do_lang_tempcode('_MISSING_RESOURCE','workflow_content->'.strval($content_id)));
	}

	// Now use it to find this content's validation field
	$hooks=find_all_hooks('systems','content_meta_aware');
	$found=false;		// Guilty until proven innocent again
	foreach (array_keys($hooks) as $hook)
	{
		// Skip if this is not the hook we're after
		if ($hook != $content_details[0]['source_type']) continue;

		// Otherwise load and instantiate the hook
		require_code('hooks/systems/content_meta_aware/'.filter_naughty_harsh($hook));
		$ob=object_factory('Hook_content_meta_aware_'.filter_naughty_harsh($hook),true);
		if (is_null($ob)) continue;		// Bail out if the hook fails

		// Grab information about the hook
		$info=$ob->info();
		$content_table = $info['table'];
		$content_field = $info['id_field'];
		if (array_key_exists('validated_field',$info))
		{
			$content_validated_field = $info['validated_field'];
		}
		else
		{
			// Fall back to 'validated' if nothing is specified
			$content_validated_field = 'validated';
		}

		if ($info['id_field_numeric'])		// See if we need to extract a number from the provided ID string
		{
			// If so then flag it with a shorter name, and use a different
			// name for the converted ID (ocPortal avoids dynamic typing)
			$numeric = true;
			$numeric_id = intval($content_details[0]['source_id']);		// Errors arise from passing an object, but should be noticed by type checker
		}
		else
		{
			// Otherwise set the flag as false. We've already got a string.
			$numeric = false;
		}
	}

	// Now we have the details required to lookup this entry, wherever it
	// is. Let's get its current validation status and compare to what
	// the workflow would have it be
	if ($numeric)
	{
		$content_is_validated = $GLOBALS['SITE_DB']->query_select($content_table,array($content_validated_field),array($content_field=>$numeric_id),'',1);
	}
	else
	{
		$content_is_validated = $GLOBALS['SITE_DB']->query_select($content_table,array($content_validated_field),array($content_field=>$content_details[0]['source_id']),'',1);
	}
	// Make sure we've actually found something
	if ($content_is_validated==array())
	{
		$source_id=$content_details[0]['source_id'];
		$validated_field=$source_id->content_validated_field;
		warn_exit(do_lang_tempcode('_MISSING_RESOURCE',$content_table.'->'.$content_field.'->'.$validated_field));
	}
	// In order for content to go live all points must be approved
	// See if all points have been approved. If so, none will have
	// status 0
	$all_points_approved = false;
	if ($GLOBALS['SITE_DB']->query_select('workflow_content_status',array('workflow_approval_name'),array('workflow_content_id'=>$content_id,'status_code'=>0))==array())
	{
		$all_points_approved = true;
	}

	// We need to act if the validation status is different to the total
	// completion of the workflow
	if (($content_is_validated[0][$content_validated_field]==1)!=$all_points_approved)
	{
		$success_message = $all_points_approved? do_lang('APPROVAL_COMPLETE') : do_lang('APPROVAL_REVOKED');
		$GLOBALS['SITE_DB']->query_update($content_table,array($content_validated_field=>$all_points_approved?1:0),array($content_field=>$content_details[0]['source_id']),'',1);
	}

	///////////////////////////////////////////
	// Now inform members about this content //
	///////////////////////////////////////////
	// Make a nicely formatted list of the statuses
	$status_list = '';
	foreach ($all_approval_statuses as $point=>$status)
	{
		$status_list .= get_translated_text($point).': ';
		$status_list .= ($status==1)?'approved':'not approved';
		$status_list .= ', ';
	}

	// At last we can send the email
	require_code('notifications');
	if (count($send_to_members) > 0)
	{
		$success_message .= do_lang('APPROVAL_CHANGED_NOTIFICATIONS');
	}
	//require_code('developer_tools');
	//inspect($emails);
	$subject=do_lang('APPROVAL_EMAIL_SUBJECT',/*TODO: Should pass title in, for unique email subject line*/NULL,NULL,NULL,get_site_default_lang());
	$body=do_lang('APPROVAL_EMAIL_BODY',post_param('http_referer',ocp_srv('HTTP_REFERER')),$status_list,$workflow_notes,get_site_default_lang());
	dispatch_notification('workflow_step',NULL/*strval($workflow_id)*/,$subject,$body,$send_to_members);

	// Finally return a success message
	$return_url = strip_tags(post_param('return_url'));
	return redirect_screen(new ocp_tempcode(),$return_url,$success_message);

}

/**
 * Returns a form field to choose the desired workflow (if there is more than
 * one in the system).
 *
 * @param  boolean		Whether to include an option for inheriting
 * @param  boolean		Whether to include an option for leaving it alone
 * @return tempcode		The UI for choosing a workflow (if needed)
 */
function workflow_choose_ui($include_inherit=false,$include_current=false)
{
	// Grab the necessary code
	require_code('workflows');
	require_lang('workflows');

	// Find out which workflows are available
	$all_workflows = get_all_workflows();

	// Only give an option to select a workflow if there is more
	// than one available
	if (count($all_workflows) > 1)
	{
		// Grab the default workflow
		$def = get_default_workflow();
		$workflows = new ocp_tempcode();

		// If we've been asked to show a "current" option then add that
		if ($include_current)
		{
			$workflows->attach(form_input_list_entry('wf_-2',true,do_lang('KEEP_WORKFLOW'),false,false));
		}

		// If we've been asked to show an "inherit" option then add that
		if ($include_inherit)
		{
			$workflows->attach(form_input_list_entry('wf_-1',!$include_current,do_lang('INHERIT_WORKFLOW'),false,false));
		}

		// Get all of the workflows we have
		foreach ($all_workflows as $id=>$workflow)
		{
			$workflows->attach(form_input_list_entry('wf_'.strval($id),(!$include_inherit && !$include_current && $id==$def),$workflow,false,false));
		}

		// Return a list entry to choose from
		$help = $include_inherit? do_lang('INHERIT_WORKFLOW_HELP') : '';
		$help .= $include_current? do_lang('CURRENT_WORKFLOW_HELP') : '';
		return form_input_list(do_lang_tempcode('USE_WORKFLOW'),do_lang_tempcode('USE_WORKFLOW_DESCRIPTION', $help),'workflow',$workflows,NULL,false);
	}
	elseif (count($all_workflows) == 1)
	{
		return form_input_hidden('workflow','wf_'.current(array_keys($all_workflows)));
	}
	else
	{
		return new ocp_tempcode();
	}
}

/**
 * Returns the workflow that the given content is in. This is useful for putting
 * a workflow on a container, like a gallery, and applying it to its contents,
 * like images.
 *
 * @param  string			The type of content (as specified when it was entered into the workflow system)
 * @param  string			The ID of the content (as specified when it was entered into the workflow system)
 * @return ?AUTO_LINK	The ID of the workflow that this content is in (NULL: not found)
 */
function get_workflow_of_content($type,$id)
{
	return $GLOBALS['SITE_DB']->query_value_null_ok('workflow_content','workflow_name',array('source_type'=>$type,'source_id'=>$id));
}

/**
 * Returns whether the given user (default: current member) can choose the
 * workflow to apply to some content they're submitting/editing.
 *
 * @param  ?MEMBER		Member (NULL: current member)
 * @return boolean		Whether the user has permission or not
 */
function can_choose_workflow($user=NULL)
{
	// Sort out the user
	if (is_null($user)) $user=get_member();

	// We currently use access to the workflow management page as the defining
	// criterion
	return has_actual_page_access(get_member(), "admin_workflow", get_module_zone("admin_workflow"));
}

/**
 * This will remove the given content from the workflow system. This is useful
 * to call from content deletion functions. NOTE: This is not the same as
 * approving the content, since the validation will remain unchanged.
 *
 * @param  string		The type of the content, as defined in the workflow_content table
 * @param  string		The ID of the content, as defined in the workflow content table
 */
function remove_content_from_workflows($type,$id)
{
	$content_id = get_workflow_content_id($type, $id);
	$GLOBALS['SITE_DB']->query_delete('workflow_content',array('id'=>$content_id));
	$GLOBALS['SITE_DB']->query_delete('workflow_content_status',array('workflow_content_id'=>$content_id));
}
