Files
resourcespace/include/annotation_functions.php
2025-07-18 16:20:14 +07:00

557 lines
16 KiB
PHP

<?php
/**
* Get annotation by ID
*
* @param integer $ref Annotation ID
*
* @return array
*/
function getAnnotation($ref)
{
if (0 >= $ref) {
return array();
}
$return = ps_query("SELECT " . columns_in("annotation") . " FROM annotation WHERE ref = ?", array("i",$ref));
if (0 < count($return)) {
$return = $return[0];
}
return $return;
}
/**
* General annotations search functionality
*
* @uses ps_query()
*
* @param integer $resource
* @param integer $resource_type_field
* @param integer $user
* @param integer $page
*
* @return array
*/
function getAnnotations($resource = 0, $resource_type_field = 0, $user = 0, $page = 0)
{
if (!is_numeric($resource) || !is_numeric($resource_type_field) || !is_numeric($user) || !is_numeric($page)) {
return array();
}
$sql_where_clause = '';
$parameters = array();
if (0 < $resource) {
$sql_where_clause = " resource = ?";
$parameters = array("i",$resource);
}
if (0 < $resource_type_field) {
if ('' != $sql_where_clause) {
$sql_where_clause .= ' AND';
}
$sql_where_clause .= " resource_type_field = ?";
$parameters = array_merge($parameters, array("i",$resource_type_field));
}
if (0 < $user) {
if ('' != $sql_where_clause) {
$sql_where_clause .= ' AND';
}
$sql_where_clause .= " user = ?";
$parameters = array_merge($parameters, array("i",$user));
}
if (0 < $page) {
if ('' != $sql_where_clause) {
$sql_where_clause .= ' AND';
}
$sql_where_clause .= " page = ?";
$parameters = array_merge($parameters, array("i",$page));
}
if ('' != $sql_where_clause) {
$sql_where_clause = "WHERE {$sql_where_clause}";
}
return ps_query("SELECT " . columns_in("annotation") . " FROM annotation {$sql_where_clause}", $parameters);
}
/**
* Get number of annotations available for a resource.
*
* Note: multi page resources will show the total number (ie. all pages)
*
* @uses ps_value()
*
* @param integer $resource Resource ID
*
* @return integer
*/
function getResourceAnnotationsCount($resource)
{
if (!is_numeric($resource) || 0 >= $resource) {
return 0;
}
return (int) ps_value("SELECT count(ref) AS `value` FROM annotation WHERE resource = ?", array("i",$resource), 0);
}
/**
* Get annotations for a specific resource
*
* @param integer $resource Resource ID
* @param integer $page Page number of a document. Non documents will have 0
*
* @return array
*/
function getResourceAnnotations($resource, $page = 0)
{
if (0 >= $resource) {
return array();
}
$parameters = array("i",$resource);
$sql_page_filter = 'AND `page` IS NULL';
if (0 < $page) {
$sql_page_filter = "AND `page` IS NOT NULL AND `page` = ?";
$parameters = array_merge($parameters, array("i",$page));
}
return ps_query("SELECT " . columns_in("annotation") . " FROM annotation WHERE resource = ? {$sql_page_filter}", $parameters);
}
/**
* Create an array of Annotorious annotation objects which can be JSON encoded and passed
* directly to Annotorious
*
* @param integer $resource Resource ID
* @param integer $page Page number of a document
*
* @return array
*/
function getAnnotoriousResourceAnnotations($resource, $page = 0)
{
global $baseurl;
$annotations = array();
$can_view_fields = canSeeAnnotationsFields();
/*
Build an annotations array of Annotorious annotation objects
IMPORTANT: until ResourceSpace will have text fields implemented as nodes, text should be left blank.
NOTE: src attribute is generated per resource (dummy source) to avoid issues when source is
loaded from download.php
*/
foreach (getResourceAnnotations($resource, $page) as $annotation) {
if (in_array($annotation['resource_type_field'], $can_view_fields)) {
$annotations[] = array(
'src' => "{$baseurl}/annotation/resource/{$resource}",
'text' => '',
'shapes' => array(
array(
'type' => 'rect',
'geometry' => array(
'x' => (float) $annotation['x'],
'y' => (float) $annotation['y'],
'width' => (float) $annotation['width'],
'height' => (float) $annotation['height'],
)
)
),
'editable' => annotationEditable($annotation),
// Custom ResourceSpace properties for Annotation object
'ref' => (int) $annotation['ref'],
'resource' => (int) $annotation['resource'],
'resource_type_field' => (int) $annotation['resource_type_field'],
'page' => (int) $annotation['page'],
'tags' => getAnnotationTags($annotation),
);
}
}
return $annotations;
}
/**
* Check if an annotation can be editable (add/ edit/ remove) by the user
*
* @uses checkPermission_anonymoususer()
*
* @param array $annotation
*
* @return boolean
*/
function annotationEditable(array $annotation)
{
debug(sprintf('[annotations][fct=annotationEditable] $annotation = %s', json_encode($annotation)));
global $userref;
$add_operation = !isset($annotation['user']);
$field_edit_access = metadata_field_edit_access($annotation['resource_type_field']);
/* Non-admin edit authorisation is valid when:
- user is just adding a new annotation
- when editing/removing an existing annotation, the annotation was created by the user itself
*/
$non_admin_athz = ($add_operation || $userref == $annotation['user']);
// Anonymous users cannot edit by default. They can only edit if they are allowed CRUD operations
if (checkPermission_anonymoususer()) {
return $non_admin_athz && $field_edit_access;
}
return (checkperm('a') || $non_admin_athz) && $field_edit_access;
}
/**
* Get all tags of an annotation. Checks if a tag is attached to the resource,
* allowing the user to search by it which is represented by the virtual column
* "tag_searchable"
*
* @param array $annotation
*
* @return array
*/
function getAnnotationTags(array $annotation)
{
$resource_ref = $annotation['resource'];
$annotation_ref = $annotation['ref'];
$parameters = array("i", $resource_ref, "i", $annotation_ref);
return ps_query("
SELECT " . columns_in("node", "n") . ",
(SELECT 'yes' FROM resource_node WHERE resource = ? AND node = ref) AS tag_searchable
FROM node AS n
WHERE ref IN (SELECT node FROM annotation_node WHERE annotation = ?);", $parameters);
}
/**
* Delete annotation
*
* @see getAnnotation()
*
* @uses annotationEditable()
* @uses getAnnotationTags()
* @uses delete_resource_nodes()
* @uses db_begin_transaction()
* @uses db_end_transaction()
*
* @param array $annotation Annotation array as returned by getAnnotation()
*
* @return boolean
*/
function deleteAnnotation(array $annotation)
{
if (!annotationEditable($annotation)) {
return false;
}
$annotation_ref = $annotation['ref'];
$parameters = array("i",$annotation_ref);
$nodes_to_remove = array();
foreach (getAnnotationTags($annotation) as $tag) {
$nodes_to_remove[] = $tag['ref'];
}
db_begin_transaction("deleteAnnotation");
if (0 < count($nodes_to_remove)) {
delete_resource_nodes($annotation['resource'], $nodes_to_remove);
}
ps_query("DELETE FROM annotation_node WHERE annotation = ?", $parameters);
ps_query("DELETE FROM annotation WHERE ref = ?", $parameters);
db_end_transaction("deleteAnnotation");
return true;
}
/**
* Create new annotations based on Annotorious annotation
*
* NOTE: Annotorious annotation shape is an array but at the moment they use only the first shape found
*
* @param array $annotation
*
* @return boolean|integer Returns false on failure OR the ref of the newly created annotation
*/
function createAnnotation(array $annotation)
{
debug(sprintf('[annotations][fct=createAnnotation] Param $annotation = %s', json_encode($annotation)));
global $userref;
if (!annotationEditable($annotation)) {
debug('[annotations][fct=createAnnotation][warn] annotation not editable');
return false;
}
debug('[annotations][fct=createAnnotation] attempting to create annotation...');
// ResourceSpace specific properties
$resource = $annotation['resource'];
$resource_type_field = $annotation['resource_type_field'];
$page = (isset($annotation['page']) && 0 < $annotation['page'] ? $annotation['page'] : null);
$tags = $annotation['tags'] ?? [];
// Annotorious annotation
$x = $annotation['shapes'][0]['geometry']['x'];
$y = $annotation['shapes'][0]['geometry']['y'];
$width = $annotation['shapes'][0]['geometry']['width'];
$height = $annotation['shapes'][0]['geometry']['height'];
ps_query(
'INSERT INTO annotation (resource, resource_type_field, user, x, y, width, height, page) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[
'i', $resource,
'i', $resource_type_field,
'i', $userref,
'd', $x,
'd', $y,
'd', $width,
'd', $height,
'i', $page,
]
);
$annotation_ref = sql_insert_id();
debug('[annotations][fct=createAnnotation] annotation_ref = ' . json_encode($annotation_ref));
if (0 == $annotation_ref) {
debug('[annotations][fct=createAnnotation][warn] Unable to create annotation');
return false;
}
// Prepare tags before association by adding new nodes to dynamic keywords list (if permissions allow it)
$prepared_tags = prepareTags($tags);
// Add any tags associated with it
if (0 < count($tags)) {
addAnnotationNodes($annotation_ref, $prepared_tags);
add_resource_nodes($resource, array_column($prepared_tags, 'ref'), false);
}
return $annotation_ref;
}
/**
* Update annotation and its tags if needed
*
* @uses annotationEditable()
* @uses getAnnotationTags()
* @uses delete_resource_nodes()
* @uses addAnnotationNodes()
* @uses add_resource_nodes()
* @uses db_begin_transaction()
* @uses db_end_transaction()
*
* @param array $annotation
*
* @return boolean
*/
function updateAnnotation(array $annotation)
{
debug(sprintf('[annotations][fct=updateAnnotation] Param $annotation = %s', json_encode($annotation)));
if (!isset($annotation['ref']) || !annotationEditable($annotation)) {
return false;
}
global $userref;
// ResourceSpace specific properties
$annotation_ref = $annotation['ref'];
$resource_type_field = $annotation['resource_type_field'];
$resource = $annotation['resource'];
$page = (isset($annotation['page']) && 0 < $annotation['page'] ? $annotation['page'] : null);
$tags = $annotation['tags'] ?? [];
// Annotorious annotation
$x = $annotation['shapes'][0]['geometry']['x'];
$y = $annotation['shapes'][0]['geometry']['y'];
$width = $annotation['shapes'][0]['geometry']['width'];
$height = $annotation['shapes'][0]['geometry']['height'];
ps_query(
'UPDATE annotation SET resource_type_field = ?, user = ?, x = ?, y = ?, width = ?, height = ?, page = ? WHERE ref = ?',
[
'i', $resource_type_field,
'i', $userref,
'd', $x,
'd', $y,
'd', $width,
'd', $height,
'i', $page,
'i', $annotation_ref,
]
);
// Delete existing associations
$nodes_to_remove = array();
foreach (getAnnotationTags($annotation) as $tag) {
$nodes_to_remove[] = $tag['ref'];
}
db_begin_transaction("updateAnnotation");
if (0 < count($nodes_to_remove)) {
delete_resource_nodes($resource, $nodes_to_remove);
}
ps_query("DELETE FROM annotation_node WHERE annotation = ?", ['i', $annotation_ref]);
// Add any tags associated with this annotation
if (0 < count($tags)) {
// Prepare tags before association by adding new nodes to
// dynamic keywords list (if permissions allow it)
$prepared_tags = prepareTags($tags);
// Add new associations
addAnnotationNodes($annotation_ref, $prepared_tags);
add_resource_nodes($resource, array_column($prepared_tags, 'ref'), false);
}
db_end_transaction("updateAnnotation");
return true;
}
/**
* Add relations between annotation and nodes
*
* @param integer $annotation_ref The annotation ID in ResourceSpace
* @param array $nodes List of node structures {@see get_nodes()}. Only the "ref" property is required.
*
* @return boolean
*/
function addAnnotationNodes($annotation_ref, array $nodes)
{
if (0 === count($nodes)) {
return false;
}
$query_insert_values = '';
$parameters = [];
foreach ($nodes as $node) {
$query_insert_values .= ',(?, ?)';
$parameters = array_merge($parameters, ['i', $annotation_ref, 'i', $node['ref']]);
}
$query_insert_values = substr($query_insert_values, 1);
ps_query("INSERT INTO annotation_node (annotation, node) VALUES {$query_insert_values}", $parameters);
return true;
}
/**
* Utility function which allows annotation tags to be prepared (i.e make sure they are all valid nodes)
* before creating associations between annotations and tags
*
* @uses checkperm()
* @uses get_resource_type_field()
* @uses set_node()
* @uses get_node()
*
* @param array $dirty_tags Original array of tags. These can be (in)valid tags/ new tags.
* IMPORTANT: a tag should have the same structure as a node
*
* @return array
*/
function prepareTags(array $dirty_tags)
{
if (0 === count($dirty_tags)) {
return array();
}
global $annotate_fields;
$clean_tags = array();
foreach ($dirty_tags as $dirty_tag) {
// Check minimum required information for a node
if (
!isset($dirty_tag['resource_type_field'])
|| 0 >= $dirty_tag['resource_type_field']
|| !in_array($dirty_tag['resource_type_field'], $annotate_fields)
) {
continue;
}
if (!isset($dirty_tag['name']) || '' == $dirty_tag['name']) {
continue;
}
// No access to field? Next...
if (!metadata_field_view_access($dirty_tag['resource_type_field'])) {
continue;
}
// New node?
if (is_null($dirty_tag['ref']) || (is_string($dirty_tag['ref']) && '' == $dirty_tag['ref'])) {
$dirty_field_data = get_resource_type_field($dirty_tag['resource_type_field']);
// Only dynamic keywords lists are allowed to create new options from annotations if permission allows it
if (
!(
FIELD_TYPE_DYNAMIC_KEYWORDS_LIST == $dirty_field_data['type']
&& !checkperm("bdk{$dirty_tag['resource_type_field']}")
)
) {
continue;
}
// Create new node but avoid duplicates
$new_node_id = set_node(null, $dirty_tag['resource_type_field'], $dirty_tag['name'], null, null);
if (false !== $new_node_id && is_numeric($new_node_id)) {
$dirty_tag['ref'] = $new_node_id;
$clean_tags[] = $dirty_tag;
}
continue;
}
// Existing tags
$found_node = array();
if (
get_node((int) $dirty_tag['ref'], $found_node)
&& $found_node['resource_type_field'] == $dirty_tag['resource_type_field']
) {
$clean_tags[] = $found_node;
}
}
return $clean_tags;
}
/**
* Add annotation count to a search result set
*
* @param array $items Array of search results returned by do_search()
*
*/
function search_add_annotation_count(&$result)
{
$annotations = ps_query(
"SELECT resource, count(*) as annocount
FROM annotation
WHERE resource IN (" . ps_param_insert(count($result)) . ")
GROUP BY resource",
ps_param_fill(array_column($result, "ref"), "i")
);
$res_annotations = array_column($annotations, "annocount", "resource");
foreach ($result as &$resource) {
$resource["annotation_count"] = $res_annotations[$resource["ref"]] ?? 0;
}
}