You are here

photos_access.module in Album Photos 6.0.x

Implementation of photos_access.module.

File

photos_access/photos_access.module
View 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

Namesort descending 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.