photos_access.module in Album Photos 6.0.x
Same filename and directory in other branches
Implementation of photos_access.module.
File
photos_access/photos_access.moduleView source
<?php
/**
* @file
* Implementation of photos_access.module.
*/
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CssCommand;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StreamWrapper\PrivateStream;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\Url;
use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
use Drupal\node\NodeInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Implements hook_help().
*/
function photos_access_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.photos_access':
$output = '';
$output .= '<p>' . t('Advanced photo access and privacy settings.') . '</p>';
$output .= '<p>' . t('The Album Photos module comes with the Photo Access
sub-module that provides settings for each album including open, locked,
designated users, or password required.') . '</p>';
$output .= '<p>' . t('See the <a href="@project_page">project page on Drupal.org</a> for more details.', [
'@project_page' => 'https://www.drupal.org/project/photos',
]) . '</p>';
return $output;
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for form_node.
*/
function photos_access_form_node_form_alter(&$form, FormStateInterface &$form_state, $form_id) {
// Get form node.
/** @var \Drupal\node\Entity\Node $node */
$node = $form_state
->getFormObject()
->getEntity();
if ($node && ($node_type = $node
->getType())) {
if (\Drupal::config('photos.settings')
->get('photos_access_' . $node_type)) {
$nid = $node
->id();
$form['photos_privacy'] = [
'#type' => 'details',
'#title' => t('Privacy'),
'#open' => TRUE,
'#weight' => 1,
'#tree' => TRUE,
];
// Access record(s) id.
$form['photos_privacy']['access_id'] = [
'#type' => 'value',
'#value' => isset($node->photos_privacy['access_id']) ? $node->photos_privacy['access_id'] : 0,
];
$form['photos_privacy']['vid'] = [
'#type' => 'value',
'#value' => isset($node->photos_privacy['vid']) ? $node->photos_privacy['vid'] : 0,
];
$form['photos_privacy']['eid'] = [
'#type' => 'value',
'#value' => isset($node->photos_privacy['eid']) ? $node->photos_privacy['eid'] : 0,
];
$old = [];
if ($nid) {
// Check collaborators and designated users.
if (!isset($node->photos_privacy['access_id']) || !($photos_album_access_id = $node->photos_privacy['access_id'])) {
$db = \Drupal::database();
$photos_album_access_id = $db
->query("SELECT id FROM {photos_access_album} WHERE nid = :nid", [
':nid' => $nid,
])
->fetchField();
}
if ($photos_album_access_id) {
$old['updateuser'] = _photos_access_userlist($photos_album_access_id, TRUE);
$old['viewuser'] = _photos_access_userlist($photos_album_access_id, FALSE);
}
}
$default_viewid = isset($node->photos_privacy['viewid']) ? $node->photos_privacy['viewid'] : 0;
// @todo add option(s) for external file systems as needed.
$privacy_options = [
0 => t('Open'),
1 => t('Locked'),
2 => t('Designated users'),
3 => t('Password required'),
];
// Prep role options. Roles with edit any can already edit this album.
// @note if "Authenticated user" has permission other roles wont inherit
// it automatically. A work-around is to uncheck authenticated user and
// check all other desired roles, then check authenticated users and save.
// Also, looks like these will be removed.
// @see https://www.drupal.org/project/drupal/issues/2025089
$user_roles_with_edit_own = user_role_names(TRUE, 'edit own photo');
$user_roles_with_edit_any = user_role_names(TRUE, 'edit any photo');
$role_options = array_diff($user_roles_with_edit_own, $user_roles_with_edit_any);
if (!empty($role_options)) {
$privacy_options[4] = t('Role access');
}
$form['photos_privacy']['viewid'] = [
'#type' => 'radios',
'#title' => t('Privacy'),
'#default_value' => $default_viewid,
'#options' => $privacy_options,
'#prefix' => '<div id="photos_access_privacy">',
'#suffix' => '</div>',
'#ajax' => [
'callback' => 'photos_access_privacy_form_ajax',
'event' => 'change',
'progress' => [
'type' => 'throbber',
'message' => NULL,
],
],
];
// Prep password field.
$classes = ' class="photos-access-password photos-access-hidden-field"';
if ($default_viewid == 3) {
$classes = ' class="photos-access-password"';
}
$form['photos_privacy']['pass'] = [
'#type' => 'password',
'#title' => t('Password'),
'#default_value' => isset($node->photos_privacy['pass']) ? $node->photos_privacy['pass'] : '',
'#prefix' => '<div id="photos-access-password"' . $classes . '>',
'#suffix' => '</div>',
];
// Prep designated users field.
$classes = ' class="photos-access-view-users photos-access-hidden-field"';
if ($default_viewid == 2) {
$classes = ' class="photos-access-view-users"';
}
$userhelp = t('Separated by commas. eg: username1,username2,username3.');
$form['photos_privacy']['viewuser'] = [
'#type' => 'entity_autocomplete',
'#title' => t('Designated users'),
'#description' => t('Add people who will have access to view this node.') . ' ' . (isset($old['viewuser']) ? t('@help The following users have access:', [
'@help' => $userhelp,
]) . ' ' : $userhelp),
'#target_type' => 'user',
'#tags' => TRUE,
'#default_value' => isset($node->photos_privacy['viewuser']) && !is_array($node->photos_privacy['viewuser']) ? $node->photos_privacy['viewuser'] : NULL,
'#process_default_value' => FALSE,
'#prefix' => '<div id="photos-access-viewuser"' . $classes . '>',
'#suffix' => '</div>',
];
if (!empty($old['viewuser'])) {
foreach ($old['viewuser'] as $u) {
$form['photos_privacy']['viewremove'][$u['target_id']] = [
'#type' => 'checkbox',
'#default_value' => isset($node->viewremove[$u['target_id']]) ? $node->viewremove[$u['target_id']] : '',
'#title' => t('Delete: @name', [
'@name' => render($u['username']),
]),
'#prefix' => '<div id="photos-access-remove"' . $classes . '>',
'#suffix' => '</div>',
];
}
}
$form['photos_privacy']['updateuser'] = [
'#type' => 'entity_autocomplete',
'#title' => t('Add collaborators'),
'#target_type' => 'user',
'#tags' => TRUE,
'#default_value' => isset($node->photos_privacy['updateuser']) && !is_array($node->photos_privacy['updateuser']) ? $node->photos_privacy['updateuser'] : NULL,
'#description' => t('Add people who will have the authority to edit this node.') . ' ' . (isset($old['updateuser']) ? t('@help collaboration users list:', [
'@help' => $userhelp,
]) . ' ' : $userhelp),
];
if (!empty($old['updateuser'])) {
// @todo add option to delete all collaborators.
foreach ($old['updateuser'] as $u) {
$form['photos_privacy']['updateremove'][$u['target_id']] = [
'#type' => 'checkbox',
'#default_value' => isset($node->updateremove[$u['target_id']]) ? $node->updateremove[$u['target_id']] : '',
'#title' => t('Delete: @name', [
'@name' => render($u['username']),
]),
'#prefix' => '<div id="photos_access_updateremove" class="photos-access-update-remove">',
'#suffix' => '</div>',
];
}
}
// @todo the roles option should probably only be accessible by admins or
// users with special permissions?
// Prep roles form option.
$classes = ' class="photos-access-roles photos-access-hidden-field"';
if ($default_viewid == 4) {
$classes = ' class="photos-access-roles"';
}
// Prep default options.
$default_roles = [];
if (isset($node->photos_privacy['roles'])) {
foreach ($node->photos_privacy['roles'] as $role) {
if ($role) {
$default_roles[$role] = $role;
}
}
}
if (!empty($role_options)) {
$description = t('The selected roles will have access to view,
edit and delete this gallery depending on global user permissions to
edit own photos and delete own photos.');
}
else {
$description = t('Roles with permission to "Edit own photos" are
required for this feature.');
}
$form['photos_privacy']['roles'] = [
'#type' => 'checkboxes',
'#title' => t('Roles'),
'#options' => array_map('\\Drupal\\Component\\Utility\\Html::escape', $role_options),
'#default_value' => $default_roles,
'#description' => $description,
'#prefix' => '<div id="photos-access-roles"' . $classes . '>',
'#suffix' => '</div>',
];
$form['#attached']['library'][] = 'photos_access/photos_access.node.form';
// Make sure $node->photos_privacy is available in node_insert and
// node_update.
$form['#entity_builders'][] = 'photos_access_node_builder';
// Validate password and users.
$form['#validate'][] = 'photos_access_node_validate';
}
if ($node_type == 'photos') {
// Move files if needed.
foreach (array_keys($form['actions']) as $action) {
if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
$form['actions'][$action]['#submit'][] = 'photos_access_move_files_form_submit';
}
}
}
}
}
/**
* Update privacy form when radio selection changes.
*/
function photos_access_privacy_form_ajax(&$form, FormStateInterface $form_state) {
$privacy = $form_state
->getValue('photos_privacy');
$response = new AjaxResponse();
$response
->addCommand(new CssCommand('#photos-access-viewuser', [
'display' => 'none',
]));
$response
->addCommand(new CssCommand('#photos-access-remove', [
'display' => 'none',
]));
$response
->addCommand(new CssCommand('#photos-access-password', [
'display' => 'none',
]));
$response
->addCommand(new CssCommand('#photos-access-roles', [
'display' => 'none',
]));
if ($privacy['viewid'] == 2) {
// Users.
$response
->addCommand(new CssCommand('#photos-access-viewuser', [
'display' => 'block',
]));
$response
->addCommand(new CssCommand('#photos-access-remove', [
'display' => 'block',
]));
}
elseif ($privacy['viewid'] == 3) {
// Password.
$response
->addCommand(new CssCommand('#photos-access-password', [
'display' => 'block',
]));
}
elseif ($privacy['viewid'] == 4) {
// User roles.
$response
->addCommand(new CssCommand('#photos-access-roles', [
'display' => 'block',
]));
}
return $response;
}
/**
* Form submission handler for private file management.
*
* @see photos_access_form_node_form_alter()
*/
function photos_access_move_files_form_submit(&$form, FormStateInterface $form_state) {
$node = $form_state
->getFormObject()
->getEntity();
// Check privacy settings.
$privacy = $form_state
->getValue('photos_privacy');
$public = FALSE;
if (isset($privacy['viewid']) && $privacy['viewid'] == 0) {
// Gallery is open, move files to public.
$public = TRUE;
}
// Check for private file path.
if (PrivateStream::basePath()) {
// Move files if needed.
// @todo Batch API.
photos_access_move_files($node, $public);
}
else {
if (!$public) {
// Set warning message.
\Drupal::messenger()
->addWarning(t('Warning: image files can still
be accessed by visiting the direct URL. For better security, ask your
website admin to setup a private file path.'));
}
}
}
/**
* Move files from public to private and private to public as needed.
*
* @param \Drupal\node\NodeInterface $node
* The node.
* @param bool $public
* Whether to use the default filesystem scheme or not.
*/
function photos_access_move_files(NodeInterface $node, $public = TRUE) {
$nid = $node
->id();
$db = \Drupal::database();
// Get user account.
$albumUid = $db
->query("SELECT uid FROM {node_field_data} WHERE nid = :nid", [
':nid' => $nid,
])
->fetchField();
$account = NULL;
try {
$account = \Drupal::entityTypeManager()
->getStorage('user')
->load($albumUid);
} catch (InvalidPluginDefinitionException $e) {
watchdog_exception('photos_access', $e);
} catch (PluginNotFoundException $e) {
watchdog_exception('photos_access', $e);
}
// Query all files in album.
$results = $db
->query("SELECT id FROM {photos_image_field_data} WHERE album_id = :nid", [
':nid' => $nid,
]);
// Check file wrapper.
$default_scheme = \Drupal::config('system.file')
->get('default_scheme');
$private_scheme = 'private';
$file_wrapper = $private_scheme . '://';
$new_scheme = $private_scheme;
if ($public) {
// Move files to default filesystem.
$file_wrapper = $default_scheme . '://';
$new_scheme = $default_scheme;
}
$cache_tags = [];
// @todo add a warning that changing privacy settings will move files.
foreach ($results as $result) {
$photosImage = NULL;
try {
/** @var \Drupal\photos\Entity\PhotosImage $photosImage */
$photosImage = \Drupal::entityTypeManager()
->getStorage('photos_image')
->load($result->id);
} catch (InvalidPluginDefinitionException $e) {
watchdog_exception('photos_access', $e);
} catch (PluginNotFoundException $e) {
watchdog_exception('photos_access', $e);
}
if ($photosImage && ($fids = $photosImage
->getFids())) {
foreach ($fids as $fid) {
$file = NULL;
try {
/** @var \Drupal\file\FileInterface $file */
$file = \Drupal::entityTypeManager()
->getStorage('file')
->load($fid);
} catch (InvalidPluginDefinitionException $e) {
watchdog_exception('photos_access', $e);
} catch (PluginNotFoundException $e) {
watchdog_exception('photos_access', $e);
}
if (!$file) {
// File not found.
continue;
}
$uri = $file
->GetFileUri();
if (strstr($uri, $file_wrapper)) {
// File is already in correct place.
// @note continue (check all) or break (assume the rest are also the
// same)? Mixed private and public files could be in the same album if
// private file path is setup after uploading some images.
continue;
}
// Move file.
$old_file_wrapper = \Drupal::service('stream_wrapper_manager')
->getScheme($uri) . '://';
if ($old_file_wrapper != $file_wrapper) {
$file_system = \Drupal::service('file_system');
// All we do here is change the file system scheme if needed.
$new_uri = str_replace($old_file_wrapper, $file_wrapper, $file
->getFileUri());
$dirname = $file_system
->dirname($new_uri);
$file_system
->prepareDirectory($dirname, FileSystemInterface::CREATE_DIRECTORY);
file_move($file, $new_uri);
// Clear image page cache.
$cache_tags[] = 'photos:image:' . $fid;
}
}
}
}
// Clear album page cache.
$cache_tags[] = 'photos:album:' . $nid;
// Invalidate image page and album page cache as needed.
Cache::invalidateTags($cache_tags);
}
/**
* Entity form builder to add the photos_access information to the node.
*
* @todo Remove this in favor of an entity field.
*/
function photos_access_node_builder($entity_type, NodeInterface $entity, &$form, FormStateInterface $form_state) {
if (!$form_state
->isValueEmpty('photos_privacy')) {
$entity->photos_privacy = $form_state
->getValue('photos_privacy');
}
}
/**
* Implements hook_ENTITY_TYPE_update().
*/
function photos_access_node_update(EntityInterface $entity) {
if (\Drupal::config('photos.settings')
->get('photos_access_' . $entity
->getType())) {
if (isset($entity->photos_privacy)) {
photos_access_update_access($entity, $entity->photos_privacy);
}
}
}
/**
* Implements hook_ENTITY_TYPE_insert().
*/
function photos_access_node_insert(EntityInterface $entity) {
if (\Drupal::config('photos.settings')
->get('photos_access_' . $entity
->getType())) {
if (isset($entity->photos_privacy)) {
photos_access_update_access($entity, $entity->photos_privacy);
}
}
}
/**
* Implements hook_ENTITY_TYPE_access().
*/
function photos_access_node_access(EntityInterface $entity, $operation, AccountInterface $account) {
if (\Drupal::config('photos.settings')
->get('photos_access_' . $entity
->getType())) {
// Check if album password is required.
photos_access_request_album_password();
// Check role access.
if (isset($entity->photos_privacy)) {
if ($entity->photos_privacy['viewid'] == 4 && isset($entity->photos_privacy['roles'])) {
$account_roles = $account
->getRoles();
// Check if role is selected for this album.
if (count(array_intersect($account_roles, $entity->photos_privacy['roles'])) !== 0) {
// Role access.
switch ($operation) {
case 'view':
return AccessResult::allowedIf($account
->hasPermission('view photo'))
->cachePerPermissions()
->addCacheableDependency($entity);
case 'update':
return AccessResult::allowedIf($account
->hasPermission('edit own photo'))
->cachePerPermissions()
->addCacheableDependency($entity);
case 'delete':
return AccessResult::allowedIf($account
->hasPermission('delete own photo'))
->cachePerPermissions()
->addCacheableDependency($entity);
}
}
}
}
}
// No opinion.
return AccessResult::neutral();
}
/**
* Album password check.
*/
function photos_access_request_album_password() {
$node = \Drupal::routeMatch()
->getParameter('node');
if (is_numeric($node)) {
// Views only provides the numeric nid.
// @todo find out if there is a better way to do this in views?
$node = Drupal::entityTypeManager()
->getStorage('node')
->load($node);
}
if ($node && $node instanceof NodeInterface && $node
->getType() == 'photos') {
// Use album node id.
$t = _photos_access_pass_type($node
->id());
}
elseif ($entityId = \Drupal::routeMatch()
->getRawParameter('photos_image')) {
// If viewing image check access to album node id.
$db = \Drupal::database();
$nid = $db
->query("SELECT album_id FROM {photos_image_field_data} WHERE id = :id", [
':id' => $entityId,
])
->fetchField();
$t = _photos_access_pass_type($nid);
}
if (isset($t['view'])) {
if ($t['view']->viewid == 3) {
photos_access_pass_validate($t);
}
}
}
/**
* D7 - Implements hook_node_validate().
*
* @todo Remove this in favor of entity field API.
*/
function photos_access_node_validate(&$form, FormStateInterface $form_state) {
// Old values.
// $node = $form_state->getFormObject()->getEntity();
// Check if users can be added to list.
$privacy = $form_state
->getValue('photos_privacy');
$access_id = $privacy['access_id'];
if ($access_id) {
// @todo handle all of this on submit.
if (isset($privacy['updateuser']) && !empty($privacy['updateuser'])) {
if ($collaborators = _photos_access_user_validate($privacy['updateuser'], $access_id, TRUE)) {
$form_state
->setErrorByName('privacy][updateuser', $collaborators);
}
}
if (isset($privacy['viewid']) && $privacy['viewid'] == 2) {
if (isset($privacy['viewuser']) && !empty($privacy['viewuser'])) {
if ($designated = _photos_access_user_validate($privacy['viewuser'], $access_id)) {
$form_state
->setErrorByName('privacy][viewuser', $designated);
}
}
}
}
// Check if password is empty.
if (isset($privacy['viewid']) && $privacy['viewid'] == 3) {
if (isset($privacy['pass']) && empty($privacy['pass'])) {
// Check if current password is set.
$db = \Drupal::database();
$current_pass = $db
->query("SELECT pass FROM {photos_access_album} WHERE id = :id", [
':id' => $access_id,
])
->fetchField();
if (!$current_pass) {
$form_state
->setErrorByName('privacy][pass', t('Password required.'));
}
}
}
}
/**
* Validate user access to node.
*/
function _photos_access_user_validate($users, $access_id, $collaborate = FALSE) {
$user = \Drupal::currentUser();
// @todo handle all of this on submit.
$output = '';
foreach ($users as $target) {
$uid = $target['target_id'];
if ($uid != $user
->id()) {
$db = \Drupal::database();
$access_user = $db
->query("SELECT id FROM {photos_access_user} WHERE id = :id AND uid = :uid AND collaborate = :collaborate", [
':id' => $access_id,
':uid' => $uid,
':collaborate' => $collaborate ? 1 : 0,
])
->fetchField();
if ($access_user) {
// @todo get name.
$output = t('User is already on the list: @uid.', [
'@uid' => $uid,
]);
}
}
else {
$output = t("You do not need to add your self to this list.");
}
}
return $output;
}
/**
* Update access to album.
*
* @param \Drupal\node\NodeInterface $node
* The node being updated.
* @param array $privacy_settings
* The album privacy settings.
*/
function photos_access_update_access(NodeInterface $node, array $privacy_settings) {
// @todo cleanup and simplify with access_id.
if (\Drupal::config('photos.settings')
->get('photos_access_' . $node
->getType())) {
if (!$privacy_settings['eid']) {
if ($privacy_settings['updateuser']) {
// Check if row already exists for this node.
$db = \Drupal::database();
$acc['updateid'] = $db
->query("SELECT id FROM {photos_access_album} WHERE nid = :nid", [
':nid' => $node
->id(),
])
->fetchField();
$privacy_settings['vid'] = $privacy_settings['eid'] = $acc['updateid'];
$db = \Drupal::database();
if ($acc['updateid']) {
// Update existing record.
$db
->update('photos_access_album')
->fields([
'viewid' => $privacy_settings['viewid'],
])
->condition('id', $acc['updateid'])
->execute();
}
else {
// Enter new record.
try {
$acc['updateid'] = $db
->insert('photos_access_album')
->fields([
'nid' => $node
->id(),
'viewid' => $privacy_settings['viewid'],
])
->execute();
} catch (Exception $e) {
watchdog_exception('photos_access', $e);
}
}
if ($acc['updateid']) {
_photos_access_usersave($privacy_settings['updateuser'], $acc['updateid']);
}
}
}
else {
// Remove collaborators.
if (isset($privacy_settings['updateremove']) && !empty($privacy_settings['updateremove'])) {
_photos_access_usersdel($privacy_settings['updateremove'], $privacy_settings['eid']);
}
// Save collaborators.
if (isset($privacy_settings['updateuser']) && !empty($privacy_settings['updateuser'])) {
_photos_access_usersave($privacy_settings['updateuser'], $privacy_settings['eid']);
}
$acc['updateid'] = $privacy_settings['eid'];
}
if (!$privacy_settings['vid']) {
// Double check for existing photos_access_album record.
$db = \Drupal::database();
$privacy_settings['vid'] = $privacy_settings['eid'] = $db
->query("SELECT id FROM {photos_access_album} WHERE nid = :nid", [
':nid' => $node
->id(),
])
->fetchField();
}
if (!$privacy_settings['vid']) {
// Insert new record.
$db = \Drupal::database();
try {
$acc['viewid'] = $db
->insert('photos_access_album')
->fields([
'nid' => $node
->id(),
'viewid' => isset($privacy_settings['viewid']) ? $privacy_settings['viewid'] : 0,
'pass' => isset($privacy_settings['pass']) && !empty($privacy_settings['pass']) ? md5($privacy_settings['pass']) : 0,
])
->execute();
} catch (Exception $e) {
watchdog_exception('photos_access', $e);
}
if ($privacy_settings['viewid'] && $privacy_settings['viewuser']) {
_photos_access_usersave($privacy_settings['viewuser'], $acc['viewid'], FALSE);
}
}
else {
// Update existing record.
switch ($privacy_settings['viewid']) {
case 0:
case 1:
case 4:
$db = \Drupal::database();
$db
->update('photos_access_album')
->fields([
':viewid' => $privacy_settings['viewid'],
])
->condition('id', $privacy_settings['vid'])
->execute();
// Delete designated users.
_photos_access_usersdel(0, $privacy_settings['vid'], 1);
break;
case 2:
$db = \Drupal::database();
$db
->update('photos_access_album')
->fields([
':viewid' => $privacy_settings['viewid'],
])
->condition('id', $privacy_settings['vid'])
->execute();
if ($privacy_settings['viewuser']) {
_photos_access_usersave($privacy_settings['viewuser'], $privacy_settings['vid'], FALSE);
}
if (isset($privacy_settings['viewremove'])) {
_photos_access_usersdel($privacy_settings['viewremove'], $privacy_settings['vid']);
}
break;
case 3:
// @todo add option to integrate real_aes module and encrypt passwords
// with that?
$db = \Drupal::database();
// Check existing password.
$old_pass = $db
->query("SELECT pass FROM {photos_access_album} WHERE id = :id", [
':id' => $privacy_settings['vid'],
])
->fetchField();
$pass = $old_pass;
// Check new password.
if (isset($privacy_settings['pass']) && !empty($privacy_settings['pass'])) {
$pass = md5($privacy_settings['pass']);
}
// Update password.
$db = \Drupal::database();
$query = $db
->update('photos_access_album');
$update_fields = [
'viewid' => $privacy_settings['viewid'],
];
// Check if new password.
if (!empty($pass) && $pass != $old_pass) {
$update_fields['pass'] = $pass;
}
$query
->fields($update_fields);
$query
->condition('id', $privacy_settings['vid']);
$query
->execute();
// Delete designated users.
_photos_access_usersdel(0, $privacy_settings['vid'], 1);
break;
}
}
// Add or update user roles if needed.
if (isset($privacy_settings['viewid']) && $privacy_settings['viewid'] == 4) {
// Get album data.
$album_data = $db
->query('SELECT data FROM {photos_album} WHERE album_id = :album_id', [
':album_id' => $node
->id(),
])
->fetchField();
$album_data = unserialize($album_data);
// Prep selected roles.
$album_data['photos_access_roles'] = [];
foreach ($privacy_settings['roles'] as $role) {
if ($role) {
$album_data['photos_access_roles'][] = $role;
}
}
// Update {photos_album}.data and add photos_access_roles array.
$db
->update('photos_album')
->fields([
'data' => serialize($album_data),
])
->condition('album_id', $node
->id())
->execute();
}
}
}
/**
* Implements hook_node_load().
*/
function photos_access_node_load($nodes) {
foreach ($nodes as $nid => $node) {
$db = \Drupal::database();
$result = $db
->query('SELECT * FROM {photos_access_album} WHERE nid = :nid', [
':nid' => $nid,
])
->fetchObject();
$info = [];
// @todo change privacy to photos_access to avoid potential conflict.
if ($result) {
// Node privacy settings.
$info['photos_privacy'] = [];
// @todo replace vid and eid with access_id.
$info['photos_privacy']['access_id'] = $result->id;
$info['photos_privacy']['vid'] = $result->id;
$info['photos_privacy']['eid'] = $result->id;
$info['photos_privacy']['viewid'] = $result->viewid;
if ($result->viewid == 3) {
$info['photos_privacy']['pass'] = $result->pass;
}
elseif ($result->viewid == 4) {
// Get roles.
if (!isset($node->album)) {
$album_data = $db
->query('SELECT data FROM {photos_album} WHERE album_id = :album_id', [
':album_id' => $node
->id(),
])
->fetchField();
$album_data = unserialize($album_data);
}
else {
$album_data = $node->album;
}
if (isset($album_data['photos_access_roles'])) {
$info['photos_privacy']['roles'] = $album_data['photos_access_roles'];
}
}
// Users who can collaborate.
$info['photos_privacy']['updateuser'] = _photos_access_userlist($result->id, TRUE);
// Users who can view.
$info['photos_privacy']['viewuser'] = _photos_access_userlist($result->id, FALSE);
$nodes[$nid]->photos_privacy = $info['photos_privacy'];
}
}
}
/**
* Implements hook_node_delete().
*/
function photos_access_node_delete(NodeInterface $node) {
$db = \Drupal::database();
$db
->delete('photos_access_album')
->condition('nid', $node
->id())
->execute();
if (isset($node->photos_privacy['vid'])) {
$db
->delete('photos_access_user')
->condition('id', $node->photos_privacy['vid'])
->execute();
}
if (isset($node->photos_privacy['eid'])) {
$db
->delete('photos_access_user')
->condition('id', $node->photos_privacy['eid'])
->execute();
}
}
/**
* Delete user from album access list.
*/
function _photos_access_usersdel($value, $id, $type = 0) {
$db = \Drupal::database();
if ($type) {
// Delete all designated users.
$db
->delete('photos_access_user')
->condition('id', $id)
->condition('collaborate', 0)
->execute();
}
elseif (is_array($value)) {
$count = count($value);
$i = 0;
foreach ($value as $key => $remove) {
if ($remove) {
++$i;
$db
->delete('photos_access_user')
->condition('id', $id)
->condition('uid', $key)
->execute();
}
}
if ($count == $i) {
return TRUE;
}
}
}
/**
* List of users who have access to album.
*/
function _photos_access_userlist($id, $collaborate = FALSE) {
$db = \Drupal::database();
$results = $db
->query('SELECT u.uid, u.name FROM {users_field_data} u
INNER JOIN {photos_access_user} a ON u.uid = a.uid
WHERE a.id = :id AND a.collaborate = :collaborate', [
':id' => $id,
':collaborate' => $collaborate ? 1 : 0,
]);
$users = [];
foreach ($results as $result) {
$username = [
'#markup' => $result->name,
];
try {
$account = \Drupal::entityTypeManager()
->getStorage('user')
->load($result->uid);
$username = [
'#theme' => 'username',
'#account' => $account,
];
} catch (InvalidPluginDefinitionException $e) {
watchdog_exception('photos_access', $e);
} catch (PluginNotFoundException $e) {
watchdog_exception('photos_access', $e);
}
$users[] = [
'target_id' => $result->uid,
'name' => $result->name,
'username' => $username,
];
}
return $users;
}
/**
* Save users to access album list.
*/
function _photos_access_usersave($value, $id, $collaborate = TRUE) {
$values = [];
foreach ($value as $target) {
$values[] = [
'id' => $id,
'uid' => $target['target_id'],
'collaborate' => $collaborate ? 1 : 0,
];
}
if (!empty($values)) {
// @todo check for duplicates?
// Insert users into photos access table.
$db = \Drupal::database();
$query = $db
->insert('photos_access_user')
->fields([
'id',
'uid',
'collaborate',
]);
foreach ($values as $record) {
$query
->values($record);
}
$query
->execute();
}
}
/**
* Implements hook_node_access_records().
*/
function photos_access_node_access_records(NodeInterface $node) {
if (\Drupal::config('photos.settings')
->get('photos_access_' . $node
->getType())) {
if (isset($node->photos_privacy['vid'])) {
// @todo cleanup?
$acc['updateid'] = isset($node->photos_privacy['eid']) ? $node->photos_privacy['eid'] : 0;
$acc['viewid'] = isset($node->photos_privacy['viewid']) ? $node->photos_privacy['viewid'] : '';
$acc['vid'] = $node->photos_privacy['vid'];
}
else {
$acc = isset($_SESSION['photos_access_ac_' . $node
->id()]) ? $_SESSION['photos_access_ac_' . $node
->id()] : '';
}
if (isset($acc['vid']) || isset($acc['updateid'])) {
// Author has full access to all albums they create.
$grants[] = [
'realm' => 'photos_access_author',
'gid' => $node
->getOwnerId(),
'grant_view' => 1,
'grant_update' => 1,
'grant_delete' => 1,
'priority' => 0,
];
// If viewid is 1:locked, only author can view it.
if ($acc['viewid'] != 1) {
// Open is 0.
$photos_access_gid = 0;
if ($acc['viewid'] != 0) {
// If not open use {node}.nid.
$photos_access_gid = $node
->id();
}
$grants[] = [
'realm' => 'photos_access',
'gid' => $photos_access_gid,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
}
// Access for collaborators.
if (isset($acc['updateid']) && !empty($acc['updateid'])) {
$grants[] = [
'realm' => 'photos_access_update',
'gid' => $node
->id(),
'grant_view' => 1,
'grant_update' => 1,
'grant_delete' => 0,
'priority' => 0,
];
}
return $grants;
}
if (isset($_SESSION['photos_access_ac_' . $node
->id()])) {
unset($_SESSION['photos_access_ac_' . $node
->id()]);
}
}
}
/**
* Implements hook_photos_access().
*/
function photos_access_photos_access() {
$current_path = \Drupal::service('path.current')
->getPath();
$path_args = explode('/', $current_path);
if (isset($path_args[2]) && $path_args[1] == 'node' && is_numeric($path_args[2])) {
return [
$path_args[2],
];
}
}
/**
* Implements hook_node_grants().
*/
function photos_access_node_grants(AccountInterface $account, $op) {
// Always grant access to view open albums.
$viewid = [
0,
];
// Set uid for author realm to access own albums.
$grants['photos_access_author'] = [
$account
->id(),
];
// Check for private albums that user has access to.
$db = \Drupal::database();
$result = $db
->query('SELECT a.*, b.* FROM {photos_access_album} a INNER JOIN {photos_access_user} b ON a.id = b.id WHERE b.uid = :uid', [
':uid' => $account
->id(),
]);
foreach ($result as $a) {
if ($a->collaborate) {
$updateid[] = $a->nid;
}
elseif ($a->viewid) {
$viewid[] = $a->nid;
}
}
// hook_photos_access()
// - Return array of nids to check for user access.
// - Only album nids that require password.
$args = \Drupal::moduleHandler()
->invokeAll('photos_access');
if (is_array($args)) {
foreach ($args as $arg) {
$result = $db
->query('SELECT id, nid, viewid, pass FROM {photos_access_album} WHERE nid = :nid', [
':nid' => $arg,
]);
foreach ($result as $a) {
// Password is required, check if password is set.
if ($a->viewid == 3 && isset($_SESSION[$a->nid . '_' . session_id()]) && $_SESSION[$a->nid . '_' . session_id()] == $a->pass) {
$viewid[] = $a->nid;
}
}
}
}
switch ($op) {
case 'view':
// Array of gid's for realm.
$grants['photos_access'] = $viewid;
if (isset($updateid[0])) {
$grants['photos_access_update'] = $updateid;
}
break;
case 'update':
if (isset($updateid[0])) {
$grants['photos_access_update'] = $updateid;
}
break;
}
return $grants;
}
/**
* Check validation type.
*/
function _photos_access_pass_type($id, $op = 0) {
$db = \Drupal::database();
if ($op) {
// photos.module.
$result = $db
->query('SELECT a.*, au.collaborate, n.uid, n.type, n.status FROM {photos_access_album} a
INNER JOIN {node_field_data} n ON a.nid = n.nid
INNER JOIN {photos_image_field_data} p ON p.album_id = a.nid
LEFT JOIN {photos_access_user} au ON a.id = au.id
WHERE p.id = :id', [
':id' => $id,
]);
}
else {
$result = $db
->query('SELECT a.*, au.collaborate, n.uid, n.type, n.status FROM {photos_access_album} a
INNER JOIN {node_field_data} n ON a.nid = n.nid
LEFT JOIN {photos_access_user} au ON a.id = au.id
WHERE a.nid = :id', [
':id' => $id,
]);
}
$t = [];
foreach ($result as $ac) {
if ($ac->viewid == 3) {
// Password authentication.
$t['view'] = $ac;
}
elseif ($ac->collaborate && $ac->pass) {
// Collaborate privileges.
$t['update'] = $ac;
}
else {
// Continue to node_access verification.
$t['node'] = $ac;
}
}
return $t;
}
/**
* Check password on node page.
*/
function photos_access_pass_validate($t) {
$user = \Drupal::currentUser();
// Check if admin or author.
if ($user
->id() == 1 || isset($t['view']->uid) && $t['view']->uid == $user
->id()) {
return TRUE;
}
if (isset($t['update']->pass)) {
// Check if collaborator.
$db = \Drupal::database();
$result = $db
->query('SELECT uid FROM {photos_access_user} WHERE id = :id', [
':id' => $t['update']->id,
]);
foreach ($result as $a) {
if ($a->uid == $user
->id()) {
return TRUE;
}
}
}
if ($t['view']->nid) {
if (isset($_SESSION[$t['view']->nid . '_' . session_id()])) {
// Check if password matches node password.
if ($_SESSION[$t['view']->nid . '_' . session_id()] == $t['view']->pass) {
return TRUE;
}
// If password is set, but does not match re enter password.
\Drupal::messenger()
->addMessage(t('Password has expired.'));
// Redirect.
$current_path = \Drupal::service('path.current')
->getPath();
$redirect_url = Url::fromUri('base:photos_privacy/pass/' . $t['view']->nid, [
'query' => [
'destination' => $current_path,
],
])
->toString();
$response = new RedirectResponse($redirect_url);
$response
->send();
exit;
}
else {
// If password is not set, password is required.
\Drupal::messenger()
->addMessage(t('Password required.'));
// Redirect.
$current_path = \Drupal::service('path.current')
->getPath();
if ($current_path == '/system/files') {
// If current path is /system/files redirect to photo album node.
$current_path = Url::fromRoute('entity.node.canonical', [
'node' => $t['view']->nid,
])
->toString();
}
$redirect_url = Url::fromUri('base:photos_privacy/pass/' . $t['view']->nid, [
'query' => [
'destination' => $current_path,
],
])
->toString();
$response = new RedirectResponse($redirect_url);
$response
->send();
exit;
}
}
}
/**
* Helper function to return options for views album privacy filter.
*/
function _photos_access_album_views_options() {
return [
0 => t('Open'),
1 => t('Locked'),
2 => t('Designated users'),
3 => t('Password required'),
4 => t('Role access'),
];
}
/**
* Implements hook_views_data().
*/
function photos_access_views_data() {
$data = [];
$data['photos_access_album'] = [];
$data['photos_access_album']['table'] = [];
$data['photos_access_album']['table']['group'] = t('Photos Access');
$data['photos_access_album']['table']['provider'] = 'photos_access';
// Join node_field_data.
$data['photos_access_album']['table']['join'] = [
'node_field_data' => [
'left_field' => 'nid',
'field' => 'nid',
],
'photos_image_field_data' => [
'left_field' => 'album_id',
'field' => 'nid',
],
];
// Numeric field, exposed as a field, sort, filter, and argument.
$data['photos_access_album']['viewid'] = [
'title' => t('Privacy'),
'help' => t('Album privacy setting.'),
'field' => [
'id' => 'numeric',
],
'sort' => [
'id' => 'standard',
],
'filter' => [
'id' => 'in_operator',
'options callback' => '_photos_access_album_views_options',
],
'argument' => [
'id' => 'numeric',
],
];
return $data;
}
/**
* Implements hook_filefield_paths_process_file().
*/
function photos_access_filefield_paths_process_file(EntityInterface $entity, FileFieldItemList $field, array $settings = []) {
if ($entity
->getEntityTypeId() == 'photos_image') {
// @note this must be triggered after filefield_paths.
_photos_access_move_field_image($entity, $field);
}
}
/**
* Implements hook_ENTITY_TYPE_update().
*/
function photos_access_photos_image_update(EntityInterface $entity) {
if (!\Drupal::moduleHandler()
->moduleExists('filefield_paths')) {
foreach ($entity
->getFields() as $field) {
if ($field instanceof FileFieldItemList) {
/** @var \Drupal\field\Entity\FieldConfig $definition */
$definition = $field
->getFieldDefinition();
// Ignore base fields.
if ($definition instanceof ThirdPartySettingsInterface) {
// @todo add test to replace photos_image field image on locked
// album and make sure it is still private.
_photos_access_move_field_image($entity, $field);
}
}
}
}
}
/**
* Helper function to make sure file fields respect album privacy settings.
*/
function _photos_access_move_field_image(EntityInterface $entity, FileFieldItemList $field) {
/** @var \Drupal\photos\Entity\PhotosImage $entity */
$album_node = \Drupal::entityTypeManager()
->getStorage('node')
->load($entity
->getAlbumId());
// Check album privacy settings.
if (isset($album_node->photos_privacy) && isset($album_node->photos_privacy['viewid'])) {
if ($album_node->photos_privacy['viewid'] != 0) {
$file_system = \Drupal::service('file_system');
// Check that the destination is writeable.
$stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
$wrappers = $stream_wrapper_manager
->getWrappers(StreamWrapperInterface::WRITE);
if (PrivateStream::basePath() && !empty($wrappers['private'])) {
// Make sure private scheme is used.
$file_wrapper = 'private://';
/** @var \Drupal\file\FileInterface $file */
foreach ($field
->referencedEntities() as $file) {
$old_file_wrapper = \Drupal::service('stream_wrapper_manager')
->getScheme($file
->getFileUri()) . '://';
if ($old_file_wrapper != $file_wrapper) {
$new_uri = str_replace($old_file_wrapper, $file_wrapper, $file
->getFileUri());
$dirname = $file_system
->dirname($new_uri);
$file_system
->prepareDirectory($dirname, FileSystemInterface::CREATE_DIRECTORY);
file_move($file, $new_uri);
}
}
}
else {
// Set warning that the file could not be moved to private.
\Drupal::messenger()
->addWarning(t('Warning: image files can
still be accessed by visiting the direct URL. For better security,
ask your website admin to setup a private file path.'));
}
}
}
}
Functions
Name![]() |
Description |
---|---|
photos_access_filefield_paths_process_file | Implements hook_filefield_paths_process_file(). |
photos_access_form_node_form_alter | Implements hook_form_BASE_FORM_ID_alter() for form_node. |
photos_access_help | Implements hook_help(). |
photos_access_move_files | Move files from public to private and private to public as needed. |
photos_access_move_files_form_submit | Form submission handler for private file management. |
photos_access_node_access | Implements hook_ENTITY_TYPE_access(). |
photos_access_node_access_records | Implements hook_node_access_records(). |
photos_access_node_builder | Entity form builder to add the photos_access information to the node. |
photos_access_node_delete | Implements hook_node_delete(). |
photos_access_node_grants | Implements hook_node_grants(). |
photos_access_node_insert | Implements hook_ENTITY_TYPE_insert(). |
photos_access_node_load | Implements hook_node_load(). |
photos_access_node_update | Implements hook_ENTITY_TYPE_update(). |
photos_access_node_validate | D7 - Implements hook_node_validate(). |
photos_access_pass_validate | Check password on node page. |
photos_access_photos_access | Implements hook_photos_access(). |
photos_access_photos_image_update | Implements hook_ENTITY_TYPE_update(). |
photos_access_privacy_form_ajax | Update privacy form when radio selection changes. |
photos_access_request_album_password | Album password check. |
photos_access_update_access | Update access to album. |
photos_access_views_data | Implements hook_views_data(). |
_photos_access_album_views_options | Helper function to return options for views album privacy filter. |
_photos_access_move_field_image | Helper function to make sure file fields respect album privacy settings. |
_photos_access_pass_type | Check validation type. |
_photos_access_userlist | List of users who have access to album. |
_photos_access_usersave | Save users to access album list. |
_photos_access_usersdel | Delete user from album access list. |
_photos_access_user_validate | Validate user access to node. |