mirror of
https://github.com/Piwigo/Piwigo.git
synced 2026-10-30 21:52:00 +01:00
1111 lines
26 KiB
PHP
1111 lines
26 KiB
PHP
<?php
|
|
// +-----------------------------------------------------------------------+
|
|
// | This file is part of Piwigo. |
|
|
// | |
|
|
// | For copyright and license information, please view the COPYING.txt |
|
|
// | file that was distributed with this source code. |
|
|
// +-----------------------------------------------------------------------+
|
|
|
|
function get_person_by_id(int $id)
|
|
{
|
|
$query = '
|
|
SELECT *
|
|
FROM '.PERSONS_TABLE.'
|
|
WHERE id = '.$id.'
|
|
;';
|
|
|
|
$result = pwg_query($query);
|
|
|
|
if (pwg_db_num_rows($result) == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return pwg_db_fetch_assoc($result);
|
|
}
|
|
|
|
function get_person_by_user_id(int $user_id)
|
|
{
|
|
$query = '
|
|
SELECT *
|
|
FROM '.PERSONS_TABLE.'
|
|
WHERE user_id = '.$user_id.'
|
|
;';
|
|
|
|
$result = pwg_query($query);
|
|
|
|
if (pwg_db_num_rows($result) == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return pwg_db_fetch_assoc($result);
|
|
}
|
|
|
|
function get_person_in_image_by_id(int $id)
|
|
{
|
|
$query = '
|
|
SELECT *
|
|
FROM '.IMAGE_PERSON_TABLE.'
|
|
WHERE id = '.$id.'
|
|
;';
|
|
|
|
$result = pwg_query($query);
|
|
|
|
if (pwg_db_num_rows($result) == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return pwg_db_fetch_assoc($result);
|
|
}
|
|
|
|
function get_total_people()
|
|
{
|
|
$query = '
|
|
SELECT COUNT(*) as nb
|
|
FROM '.PERSONS_TABLE.'
|
|
;';
|
|
$count_result = pwg_db_fetch_assoc(pwg_query($query));
|
|
|
|
return intval($count_result['nb']);
|
|
}
|
|
|
|
/**
|
|
* Can the current user see and search people in the gallery.
|
|
*/
|
|
function can_see_people()
|
|
{
|
|
global $conf, $user;
|
|
|
|
switch ($conf['people_access'] ?? 'nobody')
|
|
{
|
|
case 'everybody':
|
|
return true;
|
|
|
|
case 'registered-users':
|
|
return is_classic_user();
|
|
|
|
case 'groups':
|
|
$group_ids = safe_unserialize($conf['people_access_groups'] ?? array());
|
|
if (!is_array($group_ids))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$group_ids = array_filter($group_ids, function($group_id) { return preg_match('/^\d+$/', $group_id); });
|
|
if (empty($group_ids))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$query = '
|
|
SELECT COUNT(*)
|
|
FROM '.USER_GROUP_TABLE.'
|
|
WHERE user_id = '.$user['id'].'
|
|
AND group_id IN ('.implode(',', $group_ids).')
|
|
;';
|
|
list($nb_groups) = pwg_db_fetch_row(pwg_query($query));
|
|
|
|
return $nb_groups > 0;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a person.
|
|
*/
|
|
function create_person(string $name, ?int $user_id = null, bool $is_visible = true, string $source = 'manual')
|
|
{
|
|
$user_id_sql = $user_id === null ? 'NULL' : $user_id;
|
|
$name = strip_tags($name);
|
|
$query = '
|
|
INSERT INTO '.PERSONS_TABLE.'
|
|
(name, url_name, user_id, is_visible, source)
|
|
VALUES (
|
|
\''.pwg_db_real_escape_string($name).'\',
|
|
\''.pwg_db_real_escape_string(trigger_change('render_person_url', $name)).'\',
|
|
'.$user_id_sql.',
|
|
\''.boolean_to_string($is_visible).'\',
|
|
\''.pwg_db_real_escape_string($source).'\'
|
|
)
|
|
;';
|
|
|
|
pwg_query($query);
|
|
|
|
$new_person_id = pwg_db_insert_id();
|
|
pwg_activity('people', $new_person_id, 'add');
|
|
trigger_notify('create_person', $new_person_id);
|
|
return array(
|
|
'id' => $new_person_id,
|
|
);
|
|
}
|
|
|
|
function edit_person(int $id, array $data)
|
|
{
|
|
$validated_data = [];
|
|
$person = get_person_by_id($id);
|
|
|
|
if (!$person)
|
|
{
|
|
return array('code' => 404, 'error' => 'Person not found.');
|
|
}
|
|
|
|
// name
|
|
if (!empty($data['name']))
|
|
{
|
|
$name = strip_tags($data['name']);
|
|
$validated_data['name'] = pwg_db_real_escape_string($name);
|
|
$validated_data['url_name'] = pwg_db_real_escape_string(trigger_change('render_person_url', $name));
|
|
}
|
|
|
|
// is_visible
|
|
if (isset($data['is_visible']))
|
|
{
|
|
|
|
if (!is_bool($data['is_visible']))
|
|
{
|
|
return array('code' => 403, 'error' => 'is_visible must be: true or false.');
|
|
}
|
|
|
|
$validated_data['is_visible'] = boolean_to_string($data['is_visible']);
|
|
}
|
|
|
|
// source
|
|
if (!empty($data['source']))
|
|
{
|
|
if (!validate_people_source($data['source']))
|
|
{
|
|
return array('code' => 403, 'error' => 'source must be: manual, auto or imported.');
|
|
}
|
|
$validated_data['source'] = $data['source'];
|
|
}
|
|
|
|
// user_id, 0 dissociates the user
|
|
if (isset($data['user_id']))
|
|
{
|
|
if (!preg_match('/^\d+$/', $data['user_id']))
|
|
{
|
|
return array('code' => 403, 'error' => 'user_id must be a digit.');
|
|
}
|
|
|
|
if (0 == $data['user_id'])
|
|
{
|
|
$validated_data['user_id'] = null;
|
|
}
|
|
else if ($person['user_id'] != $data['user_id'])
|
|
{
|
|
$is_user_already_associated = get_person_by_user_id($data['user_id']);
|
|
|
|
if ($is_user_already_associated)
|
|
{
|
|
return array('code' => 401, 'error' => 'This user is already associated with a person.');
|
|
}
|
|
|
|
$validated_data['user_id'] = $data['user_id'];
|
|
}
|
|
}
|
|
|
|
// cover_image_person_id
|
|
if (!empty($data['cover_image_person_id']))
|
|
{
|
|
if (!preg_match('/^\d+$/', $data['cover_image_person_id']))
|
|
{
|
|
return array('code' => 403, 'error' => 'cover_image_person_id must be a digit.');
|
|
}
|
|
|
|
$cover = get_person_in_image_by_id($data['cover_image_person_id']);
|
|
if (!$cover)
|
|
{
|
|
return array('code' => 404, 'error' => 'Person in image not found.');
|
|
}
|
|
|
|
if ($cover['person_id'] != $id)
|
|
{
|
|
return array('code' => 403, 'error' => 'The cover must be an image of this person.');
|
|
}
|
|
|
|
$validated_data['cover_image_person_id'] = $data['cover_image_person_id'];
|
|
}
|
|
|
|
// save data
|
|
if (count($validated_data) === 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
single_update(
|
|
PERSONS_TABLE,
|
|
$validated_data,
|
|
array('id' => $id)
|
|
);
|
|
|
|
pwg_activity('people', $person['id'], 'edit');
|
|
trigger_notify('edit_person', get_person_by_id($id), $person);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set the same values to several people, only the fields they can share.
|
|
* The edit_person event is triggered for each person, like in edit_person().
|
|
*
|
|
* @param int[] $person_ids
|
|
* @param array $data
|
|
* - bool is_visible (optional)
|
|
* - string source (optional)
|
|
* @return true|array ['code', 'error']
|
|
*/
|
|
function edit_people(array $person_ids, array $data)
|
|
{
|
|
$person_ids = array_values(array_unique($person_ids));
|
|
if (count($person_ids) == 0)
|
|
{
|
|
return array('code' => 403, 'error' => 'No person given.');
|
|
}
|
|
|
|
foreach ($person_ids as $person_id)
|
|
{
|
|
if (!preg_match('/^\d+$/', $person_id))
|
|
{
|
|
return array('code' => 403, 'error' => 'Person ids must be digits.');
|
|
}
|
|
}
|
|
|
|
$validated_data = array();
|
|
|
|
if (isset($data['is_visible']))
|
|
{
|
|
if (!is_bool($data['is_visible']))
|
|
{
|
|
return array('code' => 403, 'error' => 'is_visible must be: true or false.');
|
|
}
|
|
$validated_data['is_visible'] = boolean_to_string($data['is_visible']);
|
|
}
|
|
|
|
if (!empty($data['source']))
|
|
{
|
|
if (!validate_people_source($data['source']))
|
|
{
|
|
return array('code' => 403, 'error' => 'source must be: manual, auto or imported.');
|
|
}
|
|
$validated_data['source'] = $data['source'];
|
|
}
|
|
|
|
$query_people = '
|
|
SELECT *
|
|
FROM '.PERSONS_TABLE.'
|
|
WHERE id IN ('.implode(',', $person_ids).')
|
|
;';
|
|
$people_before = query2array($query_people, 'id');
|
|
|
|
if (count($people_before) != count($person_ids))
|
|
{
|
|
return array('code' => 404, 'error' => 'Person not found.');
|
|
}
|
|
|
|
if (count($validated_data) == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
$set = array();
|
|
foreach ($validated_data as $field => $value)
|
|
{
|
|
$set[] = $field.' = \''.$value.'\'';
|
|
}
|
|
|
|
$query = '
|
|
UPDATE '.PERSONS_TABLE.'
|
|
SET '.implode(', ', $set).'
|
|
WHERE id IN ('.implode(',', $person_ids).')
|
|
;';
|
|
pwg_query($query);
|
|
|
|
pwg_activity('people', $person_ids, 'edit');
|
|
|
|
foreach (query2array($query_people, 'id') as $person_id => $person)
|
|
{
|
|
trigger_notify('edit_person', $person, $people_before[$person_id]);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param int[]|int $people_ids
|
|
* @return bool
|
|
*/
|
|
function delete_people($people_ids)
|
|
{
|
|
if (is_numeric($people_ids))
|
|
{
|
|
$people_ids = array($people_ids);
|
|
}
|
|
|
|
if (!is_array($people_ids) or count($people_ids) == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach ($people_ids as $person_id)
|
|
{
|
|
if (!preg_match('/^\d+$/', $person_id))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// we need the list of impacted images, to update their lastmodified like tags
|
|
$query = '
|
|
SELECT
|
|
image_id
|
|
FROM '.IMAGE_PERSON_TABLE.'
|
|
WHERE person_id IN ('.implode(',', $people_ids).')
|
|
;';
|
|
$image_ids = query2array($query, null, 'image_id');
|
|
|
|
$query = '
|
|
DELETE
|
|
FROM '.IMAGE_PERSON_TABLE.'
|
|
WHERE person_id IN ('.implode(',', $people_ids).')
|
|
;';
|
|
pwg_query($query);
|
|
|
|
$query = '
|
|
DELETE
|
|
FROM '.PERSONS_TABLE.'
|
|
WHERE id IN ('.implode(',', $people_ids).')
|
|
;';
|
|
pwg_query($query);
|
|
|
|
trigger_notify('delete_people', $people_ids);
|
|
pwg_activity('people', $people_ids, 'delete');
|
|
update_images_lastmodified($image_ids);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Merge people into a destination person, then delete them. Their photos go
|
|
* to the destination. On a photo where several of them are, one association
|
|
* is kept: the destination one first, then one with a box. The destination
|
|
* keeps its name, visibility and cover, and gets the user of a source if it
|
|
* has none.
|
|
*
|
|
* @param int $destination_id
|
|
* @param int[] $source_ids
|
|
* @return true|array ['code', 'error']
|
|
*/
|
|
function merge_people(int $destination_id, array $source_ids)
|
|
{
|
|
foreach ($source_ids as $source_id)
|
|
{
|
|
if (!preg_match('/^\d+$/', $source_id))
|
|
{
|
|
return array('code' => 403, 'error' => 'Person ids must be digits.');
|
|
}
|
|
}
|
|
|
|
$source_ids = array_values(array_diff(array_unique($source_ids), array($destination_id)));
|
|
if (count($source_ids) == 0)
|
|
{
|
|
return array('code' => 403, 'error' => 'No person to merge.');
|
|
}
|
|
|
|
$all_ids = array_merge(array($destination_id), $source_ids);
|
|
$query = '
|
|
SELECT *
|
|
FROM '.PERSONS_TABLE.'
|
|
WHERE id IN ('.implode(',', $all_ids).')
|
|
;';
|
|
$people = query2array($query, 'id');
|
|
if (count($people) != count($all_ids))
|
|
{
|
|
return array('code' => 404, 'error' => 'Person not found.');
|
|
}
|
|
|
|
// the sources lose their cover and user first, both are unique keys
|
|
$query = '
|
|
UPDATE '.PERSONS_TABLE.'
|
|
SET cover_image_person_id = NULL,
|
|
user_id = NULL
|
|
WHERE id IN ('.implode(',', $source_ids).')
|
|
;';
|
|
pwg_query($query);
|
|
|
|
// the destination gets the user of the first source that has one
|
|
if (empty($people[$destination_id]['user_id']))
|
|
{
|
|
$user_id = null;
|
|
foreach ($source_ids as $source_id)
|
|
{
|
|
if (!empty($people[$source_id]['user_id']))
|
|
{
|
|
$user_id = $people[$source_id]['user_id'];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (null !== $user_id)
|
|
{
|
|
single_update(
|
|
PERSONS_TABLE,
|
|
array('user_id' => $user_id),
|
|
array('id' => $destination_id)
|
|
);
|
|
}
|
|
}
|
|
|
|
// one association by photo: the destination one first, then one with a box
|
|
$query = '
|
|
SELECT *
|
|
FROM '.IMAGE_PERSON_TABLE.'
|
|
WHERE person_id IN ('.implode(',', $all_ids).')
|
|
ORDER BY image_id, (person_id = '.$destination_id.') DESC, (box_x1 IS NULL), id
|
|
;';
|
|
$associations_by_image = array();
|
|
foreach (query2array($query) as $association)
|
|
{
|
|
$associations_by_image[ $association['image_id'] ][] = $association;
|
|
}
|
|
|
|
$box_keys = array('box_x1', 'box_y1', 'box_x2', 'box_y2', 'image_width', 'image_height');
|
|
$to_delete = array();
|
|
$to_move = array();
|
|
$box_updates = array();
|
|
foreach ($associations_by_image as $associations)
|
|
{
|
|
$kept = array_shift($associations);
|
|
|
|
if ($kept['person_id'] != $destination_id)
|
|
{
|
|
$to_move[] = $kept['id'];
|
|
}
|
|
|
|
// the destination had no box on this photo, a source had one
|
|
if (null === $kept['box_x1'])
|
|
{
|
|
foreach ($associations as $association)
|
|
{
|
|
if (null !== $association['box_x1'])
|
|
{
|
|
$box_updates[] = array_merge(
|
|
array('id' => $kept['id']),
|
|
array_intersect_key($association, array_flip($box_keys))
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($associations as $association)
|
|
{
|
|
$to_delete[] = $association['id'];
|
|
}
|
|
}
|
|
|
|
if (count($to_delete) > 0)
|
|
{
|
|
$query = '
|
|
DELETE
|
|
FROM '.IMAGE_PERSON_TABLE.'
|
|
WHERE id IN ('.implode(',', $to_delete).')
|
|
;';
|
|
pwg_query($query);
|
|
}
|
|
|
|
if (count($to_move) > 0)
|
|
{
|
|
$query = '
|
|
UPDATE '.IMAGE_PERSON_TABLE.'
|
|
SET person_id = '.$destination_id.'
|
|
WHERE id IN ('.implode(',', $to_move).')
|
|
;';
|
|
pwg_query($query);
|
|
}
|
|
|
|
if (count($box_updates) > 0)
|
|
{
|
|
mass_updates(
|
|
IMAGE_PERSON_TABLE,
|
|
array(
|
|
'primary' => array('id'),
|
|
'update' => $box_keys,
|
|
),
|
|
$box_updates
|
|
);
|
|
}
|
|
|
|
trigger_notify('merge_people', $destination_id, $source_ids);
|
|
|
|
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
|
|
delete_people($source_ids);
|
|
refresh_people_cover(array($destination_id));
|
|
update_images_lastmodified(array_keys($associations_by_image));
|
|
pwg_activity('people', $destination_id, 'edit', array('action' => 'merge', 'merged' => $source_ids));
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Associate a person to an image, or update the box of the existing
|
|
* association. The source is set on creation only. A person without cover
|
|
* gets this association as cover, if the face is identified.
|
|
*
|
|
* @param int $image_id
|
|
* @param int $person_id
|
|
* @param array $data (all optional)
|
|
* - int box_x1, box_y1, box_x2, box_y2, image_width, image_height (all or none)
|
|
* - string source manual|auto|imported, default manual
|
|
* @return array ['id' => image_person_id, 'created' => bool] or ['code', 'error']
|
|
*/
|
|
function associate_person_to_image(int $image_id, int $person_id, array $data = array())
|
|
{
|
|
global $user;
|
|
|
|
$person = get_person_by_id($person_id);
|
|
if (!$person)
|
|
{
|
|
return array('code' => 404, 'error' => 'Person not found.');
|
|
}
|
|
|
|
$query = '
|
|
SELECT id
|
|
FROM '.IMAGES_TABLE.'
|
|
WHERE id = '.$image_id.'
|
|
;';
|
|
if (pwg_db_num_rows(pwg_query($query)) == 0)
|
|
{
|
|
return array('code' => 404, 'error' => 'Image not found.');
|
|
}
|
|
|
|
// source
|
|
$source = $data['source'] ?? 'manual';
|
|
if (!validate_people_source($source))
|
|
{
|
|
return array('code' => 403, 'error' => 'source must be: manual, auto or imported.');
|
|
}
|
|
|
|
$validated_data = validate_image_person_box($data);
|
|
if (isset($validated_data['error']))
|
|
{
|
|
return $validated_data;
|
|
}
|
|
|
|
$query = '
|
|
SELECT *
|
|
FROM '.IMAGE_PERSON_TABLE.'
|
|
WHERE image_id = '.$image_id.'
|
|
AND person_id = '.$person_id.'
|
|
;';
|
|
$existing = pwg_db_fetch_assoc(pwg_query($query));
|
|
|
|
if ($existing)
|
|
{
|
|
$image_person_id = $existing['id'];
|
|
|
|
// already associated, nothing to update
|
|
if (count($validated_data) == 0)
|
|
{
|
|
set_person_cover_if_none($person, $image_person_id);
|
|
return array(
|
|
'id' => $image_person_id,
|
|
'created' => false,
|
|
);
|
|
}
|
|
|
|
single_update(
|
|
IMAGE_PERSON_TABLE,
|
|
$validated_data,
|
|
array('id' => $image_person_id)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
single_insert(
|
|
IMAGE_PERSON_TABLE,
|
|
array_merge(
|
|
$validated_data,
|
|
array(
|
|
'image_id' => $image_id,
|
|
'person_id' => $person_id,
|
|
'source' => $source,
|
|
'tagger_user_id' => $user['id'],
|
|
)
|
|
)
|
|
);
|
|
$image_person_id = pwg_db_insert_id();
|
|
}
|
|
|
|
set_person_cover_if_none($person, $image_person_id);
|
|
|
|
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
|
|
update_images_lastmodified(array($image_id));
|
|
|
|
$created = !$existing;
|
|
trigger_notify('associate_person_to_image', get_person_in_image_by_id($image_person_id), $created);
|
|
|
|
return array(
|
|
'id' => $image_person_id,
|
|
'created' => $created,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete an association. If it was the cover of the person, the most recent
|
|
* identified association of the person becomes the cover.
|
|
*
|
|
* @param int $image_person_id
|
|
* @return true|array ['code', 'error']
|
|
*/
|
|
function dissociate_person_from_image(int $image_person_id)
|
|
{
|
|
$image_person = get_person_in_image_by_id($image_person_id);
|
|
if (!$image_person)
|
|
{
|
|
return array('code' => 404, 'error' => 'Person in image not found.');
|
|
}
|
|
|
|
$query = '
|
|
DELETE
|
|
FROM '.IMAGE_PERSON_TABLE.'
|
|
WHERE id = '.$image_person_id.'
|
|
;';
|
|
pwg_query($query);
|
|
|
|
unset_people_cover(array($image_person_id));
|
|
refresh_people_cover(array($image_person['person_id']));
|
|
|
|
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
|
|
update_images_lastmodified(array($image_person['image_id']));
|
|
|
|
trigger_notify('dissociate_person_from_image', $image_person);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Replace the people of an image, like set_tags. Everything is checked
|
|
* before writing, so an invalid list changes nothing.
|
|
*
|
|
* @param int $image_id
|
|
* @param array $people list of [person_id, box_x1, box_y1, box_x2, box_y2,
|
|
* image_width, image_height], box fields null for no box
|
|
* @return true|array ['code', 'error']
|
|
*/
|
|
function set_image_people(int $image_id, array $people)
|
|
{
|
|
$query = '
|
|
SELECT id
|
|
FROM '.IMAGES_TABLE.'
|
|
WHERE id = '.$image_id.'
|
|
;';
|
|
if (pwg_db_num_rows(pwg_query($query)) == 0)
|
|
{
|
|
return array('code' => 404, 'error' => 'Image not found.');
|
|
}
|
|
|
|
// check everything before writing
|
|
$person_ids = array();
|
|
foreach ($people as $person)
|
|
{
|
|
if (!is_array($person) or !isset($person['person_id']) or !preg_match('/^\d+$/', $person['person_id']))
|
|
{
|
|
return array('code' => 403, 'error' => 'person_id must be a digit.');
|
|
}
|
|
|
|
if (in_array($person['person_id'], $person_ids))
|
|
{
|
|
return array('code' => 403, 'error' => 'A person is given twice.');
|
|
}
|
|
|
|
$box = validate_image_person_box($person);
|
|
if (isset($box['error']))
|
|
{
|
|
return $box;
|
|
}
|
|
|
|
$person_ids[] = $person['person_id'];
|
|
}
|
|
|
|
if (count($person_ids) > 0)
|
|
{
|
|
$query = '
|
|
SELECT COUNT(*)
|
|
FROM '.PERSONS_TABLE.'
|
|
WHERE id IN ('.implode(',', $person_ids).')
|
|
;';
|
|
list($count) = pwg_db_fetch_row(pwg_query($query));
|
|
if ($count != count($person_ids))
|
|
{
|
|
return array('code' => 404, 'error' => 'Person not found.');
|
|
}
|
|
}
|
|
|
|
// dissociate the people no longer on the image
|
|
$query = '
|
|
SELECT
|
|
person_id,
|
|
id
|
|
FROM '.IMAGE_PERSON_TABLE.'
|
|
WHERE image_id = '.$image_id.'
|
|
;';
|
|
$existing = query2array($query, 'person_id', 'id');
|
|
|
|
foreach ($existing as $person_id => $image_person_id)
|
|
{
|
|
if (!in_array($person_id, $person_ids))
|
|
{
|
|
dissociate_person_from_image($image_person_id);
|
|
}
|
|
}
|
|
|
|
// associate the new people, update the box of the others
|
|
foreach ($people as $person)
|
|
{
|
|
$association = associate_person_to_image($image_id, $person['person_id'], $person);
|
|
if (isset($association['error']))
|
|
{
|
|
return $association;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the people of an image, with the association fields.
|
|
*
|
|
* @param int $image_id
|
|
* @return array
|
|
*/
|
|
function get_image_people(int $image_id)
|
|
{
|
|
$query = '
|
|
SELECT
|
|
ip.id AS image_person_id,
|
|
ip.person_id,
|
|
p.name,
|
|
p.url_name,
|
|
p.is_visible,
|
|
ip.box_x1,
|
|
ip.box_y1,
|
|
ip.box_x2,
|
|
ip.box_y2,
|
|
ip.image_width,
|
|
ip.image_height,
|
|
ip.source,
|
|
ip.tagged_on,
|
|
ip.tagger_user_id,
|
|
IF(p.cover_image_person_id = ip.id, \'true\', \'false\') AS is_cover
|
|
FROM '.IMAGE_PERSON_TABLE.' AS ip
|
|
INNER JOIN '.PERSONS_TABLE.' AS p ON p.id = ip.person_id
|
|
WHERE ip.image_id = '.$image_id.'
|
|
ORDER BY p.name
|
|
;';
|
|
|
|
return query2array($query);
|
|
}
|
|
|
|
/**
|
|
* People of an image shown in the gallery.
|
|
*/
|
|
function get_image_visible_people(int $image_id)
|
|
{
|
|
$query = '
|
|
SELECT
|
|
p.id,
|
|
p.name,
|
|
p.url_name,
|
|
ip.box_x1,
|
|
ip.box_y1,
|
|
ip.box_x2,
|
|
ip.box_y2,
|
|
ip.image_width,
|
|
ip.image_height
|
|
FROM '.IMAGE_PERSON_TABLE.' AS ip
|
|
INNER JOIN '.PERSONS_TABLE.' AS p ON p.id = ip.person_id
|
|
WHERE ip.image_id = '.$image_id.'
|
|
AND p.is_visible = \'true\'
|
|
ORDER BY p.name
|
|
;';
|
|
|
|
return query2array($query);
|
|
}
|
|
|
|
/**
|
|
* Background size and position (%) showing the face of an image_person in a square.
|
|
*/
|
|
function get_person_face_background(array $image_person)
|
|
{
|
|
if (!isset($image_person['box_x1']) or empty($image_person['image_width']) or empty($image_person['image_height']))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
$side = min(
|
|
$image_person['box_x2'] - $image_person['box_x1'],
|
|
$image_person['box_y2'] - $image_person['box_y1']
|
|
);
|
|
if ($side <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
$position = function($center, $image_size) use ($side)
|
|
{
|
|
if ($image_size <= $side)
|
|
{
|
|
return 50;
|
|
}
|
|
return max(0, min(100, ($center - $side / 2) / ($image_size - $side) * 100));
|
|
};
|
|
|
|
return array(
|
|
'size' => sprintf('%.2F', $image_person['image_width'] / $side * 100),
|
|
'x' => sprintf('%.2F', $position(($image_person['box_x1'] + $image_person['box_x2']) / 2, $image_person['image_width'])),
|
|
'y' => sprintf('%.2F', $position(($image_person['box_y1'] + $image_person['box_y2']) / 2, $image_person['image_height'])),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the images of a person, with the association fields, the thumbnail
|
|
* url and the medium url (to crop the face). Admin view: photo permissions
|
|
* are not taken into account.
|
|
*
|
|
* @param int $person_id
|
|
* @param int $per_page
|
|
* @param int $page starts at 0
|
|
* @param bool $identified only the images where the face is identified (with a box)
|
|
* @return array ['paging', 'images'] or ['code', 'error']
|
|
*/
|
|
function get_person_images(int $person_id, int $per_page = 100, int $page = 0, bool $identified = false)
|
|
{
|
|
if (!get_person_by_id($person_id))
|
|
{
|
|
return array('code' => 404, 'error' => 'Person not found.');
|
|
}
|
|
|
|
$per_page = max(1, $per_page);
|
|
$page = max(0, $page);
|
|
|
|
$where_sql = '
|
|
WHERE ip.person_id = '.$person_id;
|
|
|
|
if ($identified)
|
|
{
|
|
$where_sql.= '
|
|
AND ip.box_x1 IS NOT NULL';
|
|
}
|
|
|
|
$query = '
|
|
SELECT
|
|
i.*,
|
|
ip.id AS image_person_id,
|
|
ip.box_x1,
|
|
ip.box_y1,
|
|
ip.box_x2,
|
|
ip.box_y2,
|
|
ip.image_width,
|
|
ip.image_height,
|
|
ip.source,
|
|
ip.tagged_on
|
|
FROM '.IMAGE_PERSON_TABLE.' AS ip
|
|
INNER JOIN '.IMAGES_TABLE.' AS i ON i.id = ip.image_id
|
|
'.$where_sql.'
|
|
ORDER BY ip.tagged_on DESC, ip.id DESC
|
|
LIMIT '.$per_page.'
|
|
OFFSET '.($per_page * $page).'
|
|
;';
|
|
$result = pwg_query($query);
|
|
|
|
$images = array();
|
|
while ($row = pwg_db_fetch_assoc($result))
|
|
{
|
|
$images[] = array(
|
|
'id' => $row['id'],
|
|
'name' => $row['name'],
|
|
'file' => $row['file'],
|
|
'width' => $row['width'],
|
|
'height' => $row['height'],
|
|
'tn_url' => DerivativeImage::thumb_url($row),
|
|
'medium_url' => DerivativeImage::url(IMG_MEDIUM, new SrcImage($row)),
|
|
'image_person_id' => $row['image_person_id'],
|
|
'box_x1' => $row['box_x1'],
|
|
'box_y1' => $row['box_y1'],
|
|
'box_x2' => $row['box_x2'],
|
|
'box_y2' => $row['box_y2'],
|
|
'image_width' => $row['image_width'],
|
|
'image_height' => $row['image_height'],
|
|
'source' => $row['source'],
|
|
'tagged_on' => $row['tagged_on'],
|
|
);
|
|
}
|
|
|
|
$query = '
|
|
SELECT COUNT(*)
|
|
FROM '.IMAGE_PERSON_TABLE.' AS ip
|
|
INNER JOIN '.IMAGES_TABLE.' AS i ON i.id = ip.image_id
|
|
'.$where_sql.'
|
|
;';
|
|
list($total) = pwg_db_fetch_row(pwg_query($query));
|
|
|
|
return array(
|
|
'paging' => array(
|
|
'page' => $page,
|
|
'per_page' => $per_page,
|
|
'total' => $total,
|
|
'total_pages' => ceil($total / $per_page),
|
|
),
|
|
'images' => $images,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A person without cover gets this association as cover, if the face is
|
|
* identified (the association has a box).
|
|
*
|
|
* @param array $person row of the persons table
|
|
* @param int $image_person_id
|
|
*/
|
|
function set_person_cover_if_none(array $person, $image_person_id)
|
|
{
|
|
if (!empty($person['cover_image_person_id']))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// only an identified face can be a cover
|
|
$image_person = get_person_in_image_by_id($image_person_id);
|
|
if (!$image_person or null === $image_person['box_x1'])
|
|
{
|
|
return;
|
|
}
|
|
|
|
single_update(
|
|
PERSONS_TABLE,
|
|
array('cover_image_person_id' => $image_person_id),
|
|
array('id' => $person['id'])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* People without cover get their most recent identified association as cover.
|
|
* Called after a cover is removed, so the person keeps a face if possible.
|
|
*
|
|
* @param int[] $person_ids
|
|
*/
|
|
function refresh_people_cover(array $person_ids)
|
|
{
|
|
if (count($person_ids) == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
$query = '
|
|
SELECT id
|
|
FROM '.PERSONS_TABLE.'
|
|
WHERE id IN ('.implode(',', $person_ids).')
|
|
AND cover_image_person_id IS NULL
|
|
;';
|
|
$people_without_cover = query2array($query, null, 'id');
|
|
|
|
foreach ($people_without_cover as $person_id)
|
|
{
|
|
$query = '
|
|
SELECT id
|
|
FROM '.IMAGE_PERSON_TABLE.'
|
|
WHERE person_id = '.$person_id.'
|
|
AND box_x1 IS NOT NULL
|
|
ORDER BY tagged_on DESC, id DESC
|
|
LIMIT 1
|
|
;';
|
|
$image_person_ids = query2array($query, null, 'id');
|
|
|
|
if (count($image_person_ids) > 0)
|
|
{
|
|
single_update(
|
|
PERSONS_TABLE,
|
|
array('cover_image_person_id' => $image_person_ids[0]),
|
|
array('id' => $person_id)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unset the cover of the people whose cover is one of these associations.
|
|
*
|
|
* @param int[] $image_person_ids
|
|
*/
|
|
function unset_people_cover(array $image_person_ids)
|
|
{
|
|
if (count($image_person_ids) == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
$query = '
|
|
UPDATE '.PERSONS_TABLE.'
|
|
SET cover_image_person_id = NULL
|
|
WHERE cover_image_person_id IN ('.implode(',', $image_person_ids).')
|
|
;';
|
|
pwg_query($query);
|
|
}
|
|
|
|
// utils
|
|
|
|
/**
|
|
* Validate a box, in pixels of the image at image_width x image_height.
|
|
* box_x1, box_y1, box_x2, box_y2, image_width and image_height: all or none.
|
|
*
|
|
* @param array $data
|
|
* @return array the box, empty if there is none, or ['code', 'error']
|
|
*/
|
|
function validate_image_person_box(array $data)
|
|
{
|
|
$box_keys = array('box_x1', 'box_y1', 'box_x2', 'box_y2', 'image_width', 'image_height');
|
|
$box = array();
|
|
foreach ($box_keys as $key)
|
|
{
|
|
if (isset($data[$key]))
|
|
{
|
|
if (!preg_match('/^\d+$/', $data[$key]) or $data[$key] > 65535)
|
|
{
|
|
return array('code' => 403, 'error' => $key.' must be an integer between 0 and 65535.');
|
|
}
|
|
$box[$key] = $data[$key];
|
|
}
|
|
}
|
|
|
|
if (count($box) == 0)
|
|
{
|
|
return $box;
|
|
}
|
|
|
|
if (count($box) != count($box_keys))
|
|
{
|
|
return array('code' => 403, 'error' => 'box needs box_x1, box_y1, box_x2, box_y2, image_width and image_height.');
|
|
}
|
|
|
|
if ($box['box_x1'] >= $box['box_x2']
|
|
or $box['box_y1'] >= $box['box_y2']
|
|
or $box['box_x2'] > $box['image_width']
|
|
or $box['box_y2'] > $box['image_height'])
|
|
{
|
|
return array('code' => 403, 'error' => 'box must be inside the image, with x1 < x2 and y1 < y2.');
|
|
}
|
|
|
|
return $box;
|
|
}
|
|
|
|
function validate_people_source(string $source)
|
|
{
|
|
if (!in_array($source, ['manual', 'imported', 'auto']))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} |