monster_menus-9.0.x-dev/mm_content.inc
mm_content.inc
<?php
/**
* @file
* Functions related to the retrieval of data for monster menus
*/
use Drupal\block\Entity\Block;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Link;
use Drupal\Core\Pager\Pager;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\monster_menus\Constants;
use Drupal\monster_menus\Entity\MMTree;
use Drupal\monster_menus\GetTreeIterator\ContentCopyIter;
use Drupal\monster_menus\GetTreeIterator\ContentDeleteIter;
use Drupal\monster_menus\GetTreeIterator\ContentFindUnmodifiedHomepagesIter;
use Drupal\monster_menus\GetTreeIterator\ContentMoveIter;
use Drupal\monster_menus\GetTreeIterator\ContentTestShowpageIter;
use Drupal\monster_menus\GetTreeIterator\ContentUserCanRecycleIter;
use Drupal\monster_menus\GetTreeResults;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\user\Entity\User;
use Drupal\views\ViewExecutable;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Return a nicer version of entry names starting with '.'
*
* @param $name
* The entry name, obtained from mm_content_get_tree
* @return string
* The nicer version
*/
function mm_content_expand_name($name) {
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
// This is cumbersome, but assigning to an array is the only way that works.
$drupal_static_fast['aliases'] = &drupal_static(__FUNCTION__);
$drupal_static_fast['aliases'] = [
Constants::MM_ENTRY_NAME_DEFAULT_USER => t('[New account defaults]'),
Constants::MM_ENTRY_NAME_DISABLED_USER => t('[Disabled accounts]'),
Constants::MM_ENTRY_NAME_GROUPS => t('Permission groups'),
Constants::MM_ENTRY_NAME_LOST_FOUND => t('[Lost and found]'),
Constants::MM_ENTRY_NAME_RECYCLE => t('[Recycle bin]'),
Constants::MM_ENTRY_NAME_USERS => t('User homepages'),
Constants::MM_ENTRY_NAME_VIRTUAL_GROUP => t('[Pre-defined groups]'),
];
$drupal_static_fast['aliases'] += mm_module_invoke_all('mm_item_name');
}
if (!is_string($name)) {
// The name is not a bare string, so it can't possibly have an alias.
return $name;
}
$aliases = &$drupal_static_fast['aliases'];
if (isset($aliases[$name])) {
if (is_array($aliases[$name])) {
if (isset($aliases[$name]['callback']) && function_exists($aliases[$name]['callback'])) {
$result = call_user_func($aliases[$name]['callback'], $name);
if (!empty($result)) {
return $result;
}
}
if (!empty($aliases[$name]['name'])) {
return $aliases[$name]['name'];
}
}
else {
return $aliases[$name];
}
}
return $name;
}
/**
* Traverse the tree
*
* @param int $mmtid (1)
* Starting tree ID
* @param array $params
* An array containing parameters. The array is indexed using the constants
* below.
* - MM_GET_TREE_ADD_SELECT (none):
* A string or array of strings to add to the SELECT portion of the query
* - MM_GET_TREE_ADD_TO_CACHE (FALSE):
* Add results to the caches used by mm_content_get() and
* mm_content_get_parents()
* - MM_GET_TREE_BIAS_ANON (TRUE):
* If TRUE, assume user 0 can't read any groups (more secure)
* - MM_GET_TREE_BLOCK (''):
* Only retrieve entries that are part of one block. Defaults to all blocks.
* - MM_GET_TREE_DEPTH (-1):
* When 'mmtid' is used, a query to return all items in the tree below that
* point can be returned. This field specifies the depth of recursion:
* - 0: just the item specified by $mmtid
* - -1: all levels
* - 1: the item and its immediate children
* - N: any other number will return that many levels (can be slow)
* - MM_GET_TREE_FAKE_READ_BINS (FALSE):
* Pretend the user can read all recycle bins (used internally)
* - MM_GET_TREE_FILTER_BINS (TRUE):
* Get entries that are recycle bins
* - MM_GET_TREE_FILTER_DOTS (TRUE):
* Get all entries with names that start with '.'. If FALSE, only .Groups,
* .Users, and .Virtual are returned.
* - MM_GET_TREE_FILTER_GROUPS (TRUE):
* Get entries that are groups
* - MM_GET_TREE_FILTER_HIDDEN (FALSE):
* If TRUE, return entries with the "hidden" attribute set, even if the
* current user does not normally have permission to view them
* - MM_GET_TREE_FILTER_NORMAL (TRUE):
* Get entries that are neither groups nor in /users
* - MM_GET_TREE_FILTER_USERS (TRUE):
* Get entries in /users
* - MM_GET_TREE_HERE (none)
* An array of MM Tree IDs currently being viewed by the user. Parent
* entries will have their state set to MM_GET_TREE_STATE_EXPANDED.
* - MM_GET_TREE_ITERATOR (none):
* GetTreeIterator (or subclass) to call as each new item is found. When
* this option is used, memory is conserved by not returning anything.
* - MM_GET_TREE_PRUNE_PARENTS (FALSE):
* If TRUE, prune parents, depending upon max_parents in the block
* - MM_GET_TREE_RETURN_BINS (FALSE):
* A comma-separated list of the mmtids of any parent recycle bins
* - MM_GET_TREE_RETURN_BLOCK (FALSE):
* Attributes from the mm_tree_block table
* - MM_GET_TREE_RETURN_FLAGS (FALSE):
* Flags from the mm_tree_flags table
* - MM_GET_TREE_RETURN_KID_COUNT (FALSE):
* A count of the number of children each tree entry has
* - MM_GET_TREE_RETURN_MTIME (FALSE):
* The muid (user ID who made the last modification) and mtime (time) of the
* modification
* - MM_GET_TREE_RETURN_NODE_COUNT (FALSE):
* If TRUE, return a count of the number of nodes assigned to each item. If
* a string or array of strings, return a count of the number of nodes of
* that type.
* - MM_GET_TREE_RETURN_PERMS (none):
* If set, return whether the user can perform that action
* (MM_PERMS_READ, MM_PERMS_WRITE, MM_PERMS_SUB, MM_PERMS_APPLY,
* MM_PERMS_IS_USER, MM_PERMS_IS_GROUP, MM_PERMS_IS_RECYCLE_BIN,
* MM_PERMS_IS_RECYCLED). The requested permission can either be a single
* value or an array. If an empty array or TRUE is passed, all permissions
* are returned.
* - MM_GET_TREE_SORT (FALSE):
* If TRUE, sort the entries according to sort_idx; always TRUE when
* MM_GET_TREE_DEPTH != 0
* - MM_GET_TREE_USER (current user):
* User object to test permissions against
* - MM_GET_TREE_VIRTUAL (TRUE):
* Include virtual user list sub-entries
* - MM_GET_TREE_WHERE (none):
* Add a WHERE clause to the outermost query
* If none of ([...USERS], [...GROUPS], [...NORMAL]) is TRUE, all types are
* retrieved. MM_GET_TREE_RETURN_TREE is always TRUE.
* @return array|null
* Array of tree entries, unless MM_GET_TREE_ITERATOR is used
*/
function mm_content_get_tree($mmtid = 1, $params = NULL) {
$defaults = [
Constants::MM_GET_TREE_BLOCK => '',
Constants::MM_GET_TREE_DEPTH => -1,
Constants::MM_GET_TREE_FILTER_BINS => TRUE,
Constants::MM_GET_TREE_FILTER_DOTS => TRUE,
Constants::MM_GET_TREE_FILTER_GROUPS => FALSE,
Constants::MM_GET_TREE_FILTER_HIDDEN => FALSE,
Constants::MM_GET_TREE_FILTER_NORMAL => FALSE,
Constants::MM_GET_TREE_FILTER_USERS => FALSE,
Constants::MM_GET_TREE_HERE => NULL,
Constants::MM_GET_TREE_ITERATOR => NULL,
Constants::MM_GET_TREE_PRUNE_PARENTS => FALSE,
Constants::MM_GET_TREE_SORT => FALSE,
Constants::MM_GET_TREE_USER => \Drupal::currentUser(),
Constants::MM_GET_TREE_VIRTUAL => TRUE,
Constants::MM_GET_TREE_FAST => FALSE,
'found' => -1,
'level' => 0,
'parent_level' => -1,
'pprune' => -1,
'q' => NULL,
];
if (!is_array($params)) {
$params = [];
}
$params = array_merge($defaults, $params);
if (empty($params[Constants::MM_GET_TREE_FILTER_GROUPS]) && empty($params[Constants::MM_GET_TREE_FILTER_USERS]) && empty($params[Constants::MM_GET_TREE_FILTER_NORMAL])) {
$params[Constants::MM_GET_TREE_FILTER_GROUPS] = $params[Constants::MM_GET_TREE_FILTER_USERS] = $params[Constants::MM_GET_TREE_FILTER_NORMAL] = TRUE;
}
if ($params[Constants::MM_GET_TREE_VIRTUAL] && !mm_get_setting('user_homepages.virtual')) {
$params[Constants::MM_GET_TREE_VIRTUAL] = FALSE;
}
if (!empty($params[Constants::MM_GET_TREE_SORT]) || $params[Constants::MM_GET_TREE_DEPTH] != 0) {
mm_content_update_sort_queue();
}
return _mm_content_get_tree($mmtid, $params);
}
function _mm_content_get_tree($mmtid, &$params) {
$users_mmtid = mm_content_users_mmtid();
$have_virtual = FALSE;
if (empty($params['q'])) {
$params['q'] = _mm_content_get_tree_query($mmtid, $params);
if (is_array($params[Constants::MM_GET_TREE_HERE])) {
foreach ($params[Constants::MM_GET_TREE_HERE] as $i => $h) {
if ($h < 0) {
unset($params[Constants::MM_GET_TREE_HERE][$i]);
$have_virtual = TRUE;
if ($params[Constants::MM_GET_TREE_DEPTH] > 0) {
$params[Constants::MM_GET_TREE_DEPTH]--;
}
break;
}
elseif ($params[Constants::MM_GET_TREE_VIRTUAL] && $h == $users_mmtid && $params[Constants::MM_GET_TREE_DEPTH]) {
$have_virtual = TRUE;
}
}
}
}
$rows = [];
while ($r = $params['q']->next()) {
if (!isset($params['q']->start_level)) {
$params['q']->start_level = strlen($r->sort_idx) / Constants::MM_CONTENT_BTOA_CHARS;
}
$r->level = strlen($r->sort_idx) / Constants::MM_CONTENT_BTOA_CHARS - $params['q']->start_level + $params['q']->level_offset;
if ($r->level <= $params['parent_level']) {
$params['q']->back();
break;
}
else {
if (!isset($r->bid)) {
$r->bid = Constants::MM_MENU_DEFAULT;
$r->max_depth = $r->max_parents = -1;
}
$add = _mm_content_get_tree_recurs($r, $params, $r->{Constants::MM_PERMS_IS_GROUP} ?? FALSE, $r->{Constants::MM_PERMS_IS_USER} ?? FALSE, $last);
}
if (is_array($rows) && !empty($add) && is_array($add)) {
$rows = array_merge($rows, $add);
unset($add); // save some memory
}
if (!empty($last)) {
if ($last === 'abort') {
$params['abort'] = TRUE;
}
break;
}
}
if ($params['pprune'] > 0 && $params['found']) {
$params['pprune']--;
}
if (isset($params[Constants::MM_GET_TREE_ITERATOR])) {
return NULL;
}
if (!$params['level'] && ($have_virtual || $mmtid == $users_mmtid) && $params[Constants::MM_GET_TREE_DEPTH]) {
if ($params[Constants::MM_GET_TREE_VIRTUAL]) {
$select = Database::getConnection()->select('mm_tree', 't');
$select->addExpression('GROUP_CONCAT(DISTINCT UCASE(SUBSTR(t.name, 1, 1)) ORDER BY t.name SEPARATOR \'\')', 'letters');
$select->condition('t.parent', $users_mmtid);
$letters = $select->execute()->fetchField();
$letters = preg_replace('/[\W_]/', '', $letters, -1, $matches);
if ($matches) $letters = "~$letters";
$letters = str_split($letters);
$parent = NULL;
for ($i = 0; $i < count($rows); $i++) {
if ($rows[$i]->mmtid == $users_mmtid) {
$parent = $rows[$remainder = $i];
$parent->state &= ~Constants::MM_GET_TREE_STATE_HERE;
$last = 0;
while (++$i < count($rows) && $rows[$i]->level > $parent->level) {
if ($rows[$i]->level == $parent->level + 1) {
$letr = mb_strtoupper($rows[$i]->name[0]);
$name = ctype_alpha($letr) ? $letr : t('(other)');
if (!$last || $name != $rows[$last]->name) {
$alias = ctype_alpha($letr) ? $letr : '~';
while ($letters) {
$add = array_shift($letters);
$new = _mm_content_virtual_dir(-ord($add), $parent->mmtid, $parent->level + 1, $add == $alias ? Constants::MM_GET_TREE_STATE_EXPANDED|Constants::MM_GET_TREE_STATE_HERE : Constants::MM_GET_TREE_STATE_COLLAPSED);
$new->default_mode = $parent->default_mode;
array_splice($rows, $last = $i++, 0, [$new]); // insert virtual dir
$remainder++;
if ($add == $alias) {
break;
}
}
}
$rows[$i]->parent = $rows[$last]->mmtid;
if ($rows[$i]->state & Constants::MM_GET_TREE_STATE_EXPANDED) {
$rows[$last]->state = Constants::MM_GET_TREE_STATE_EXPANDED;
}
} // if
$remainder++;
$rows[$i]->level++;
} // while
break; // exit outer for loop
} // if
} // for
}
if (!\Drupal::currentUser()->hasPermission('administer all users')) {
$hidden_names = \Drupal::state()->get('monster_menus.hidden_user_names', []);
$dels = [];
foreach ($rows as $i => $r) {
if ($r->alias == '~') {
$other = $i;
}
elseif ($r->parent == -126 || $r->parent == $users_mmtid) { // -126 = -ord('~')
if (in_array($r->name, $hidden_names)) {
$dels[] = $i;
}
else {
unset($other);
}
}
}
if (isset($other)) {
// All 'other' rows are invisible to the user
array_unshift($dels, $other);
}
foreach (array_reverse($dels) as $i) {
array_splice($rows, $i, 1);
}
}
if ($params[Constants::MM_GET_TREE_VIRTUAL]) {
$i = $parent ? $remainder + 1 : count($rows);
foreach ($letters as $add) {
$new = _mm_content_virtual_dir(-ord($add), $users_mmtid, $parent ? $parent->level + 1 : 0, Constants::MM_GET_TREE_STATE_COLLAPSED);
if ($parent) {
$new->default_mode = $parent->default_mode;
}
array_splice($rows, $i++, 0, [$new]); // insert virtual dir
}
}
}
// if( !$params['level'] ) debug_add_dump( $rows );
return $rows;
}
/**
* Helper function for _mm_content_get_tree()/mm_content_get()
*/
function _mm_content_split_flags($flags) {
if (is_array($flags)) {
return $flags;
}
if (empty($flags)) {
return [];
}
preg_match_all('/(.*?)\|1(.*?)(?:\|2|$)/', $flags, $matches);
return $matches[0] ? array_combine($matches[1], $matches[2]) : [];
}
/**
* Helper function for _mm_content_get_tree()
*/
function _mm_content_get_tree_query($mmtid, $params) {
if (!empty($params[Constants::MM_GET_TREE_FAST])) {
$unsupported = [
Constants::MM_GET_TREE_BIAS_ANON => ['MM_GET_TREE_BIAS_ANON', TRUE],
Constants::MM_GET_TREE_FAKE_READ_BINS => ['MM_GET_TREE_FAKE_READ_BINS', TRUE],
Constants::MM_GET_TREE_BLOCK => ['MM_GET_TREE_BLOCK', TRUE],
Constants::MM_GET_TREE_FILTER_GROUPS => ['MM_GET_TREE_FILTER_GROUPS', FALSE],
Constants::MM_GET_TREE_FILTER_USERS => ['MM_GET_TREE_FILTER_USERS', FALSE],
Constants::MM_GET_TREE_FILTER_NORMAL => ['MM_GET_TREE_FILTER_NORMAL', FALSE],
Constants::MM_GET_TREE_FILTER_BINS => ['MM_GET_TREE_FILTER_BINS', FALSE],
Constants::MM_GET_TREE_FILTER_DOTS => ['MM_GET_TREE_FILTER_DOTS', FALSE],
Constants::MM_GET_TREE_NODE => ['MM_GET_TREE_NODE', TRUE],
Constants::MM_GET_TREE_ADD_SELECT => ['MM_GET_TREE_ADD_SELECT', TRUE],
Constants::MM_GET_TREE_RETURN_BINS => ['MM_GET_TREE_RETURN_BINS', TRUE],
Constants::MM_GET_TREE_RETURN_FLAGS => ['MM_GET_TREE_RETURN_FLAGS', TRUE],
Constants::MM_GET_TREE_RETURN_KID_COUNT => ['MM_GET_TREE_RETURN_KID_COUNT', TRUE],
Constants::MM_GET_TREE_RETURN_NODE_COUNT => ['MM_GET_TREE_RETURN_NODE_COUNT', TRUE],
Constants::MM_GET_TREE_RETURN_PERMS => ['MM_GET_TREE_RETURN_PERMS', TRUE],
Constants::MM_GET_TREE_WHERE => ['MM_GET_TREE_WHERE', TRUE],
];
foreach ($unsupported as $key => $test) {
assert(empty($params[$key]) === $test[1], "$test[0] is not supported in combination with MM_GET_TREE_FAST.");
}
}
$params[Constants::MM_GET_TREE_RETURN_TREE] = TRUE;
if (!empty($params[Constants::MM_GET_TREE_BLOCK])) {
$params[Constants::MM_GET_TREE_RETURN_BLOCK] = TRUE;
}
if (!is_array($params[Constants::MM_GET_TREE_HERE])) {
$params[Constants::MM_GET_TREE_HERE] = [$mmtid];
}
elseif (!count($params[Constants::MM_GET_TREE_HERE])) {
$params[Constants::MM_GET_TREE_HERE][] = $mmtid;
}
elseif ($params[Constants::MM_GET_TREE_DEPTH] != 0) {
$params[Constants::MM_GET_TREE_DEPTH] = 1;
}
$query = [];
$max = count($params[Constants::MM_GET_TREE_HERE]) - 1;
$users_mmtid = $params[Constants::MM_GET_TREE_VIRTUAL] ? mm_content_users_mmtid() : -1;
if (isset($params[Constants::MM_GET_TREE_RETURN_PERMS])) {
if (!isset($params[Constants::MM_GET_TREE_ITERATOR])) {
$params[Constants::MM_GET_TREE_RETURN_BINS] = TRUE;
$params[Constants::MM_GET_TREE_FAKE_READ_BINS] = TRUE;
}
}
else {
$params[Constants::MM_GET_TREE_RETURN_PERMS] = [Constants::MM_PERMS_IS_GROUP, Constants::MM_PERMS_IS_USER];
}
if (!empty($params[Constants::MM_GET_TREE_FAST]) && !$max && $mmtid >= 0) {
if ($tree = mm_content_get($mmtid)) {
$len = strlen($tree->sort_idx);
$compare = $params[Constants::MM_GET_TREE_DEPTH] == 0 ? 'sort_idx' : "LEFT(sort_idx, $len)";
$q = "SELECT * FROM {mm_tree} WHERE $compare = " . \Drupal::database()->quote($tree->sort_idx);
if ($params[Constants::MM_GET_TREE_DEPTH] > 0) {
$q .= ' AND LENGTH(sort_idx) < ' . ($len + $params[Constants::MM_GET_TREE_DEPTH] * Constants::MM_CONTENT_BTOA_CHARS);
}
$query[] = "$q ORDER BY sort_idx";
}
}
else {
for ($i = 0; $i <= $max; $i++) {
$mmtid = $params[Constants::MM_GET_TREE_HERE][$i];
if ($mmtid != $users_mmtid || $i == $max || $params[Constants::MM_GET_TREE_HERE][$i + 1] >= 0) {
$params2 = $params;
if ($mmtid < 0) {
$ch = chr(-$mmtid);
$re = $ch == '~' ? "t.name REGEXP '^[^[:alpha:]]'" : "UCASE(t.name) LIKE '$ch%'";
$params2[Constants::MM_GET_TREE_INNER_FILTER] = " AND $re";
$params2[Constants::MM_GET_TREE_DEPTH] = 1;
$params2[Constants::MM_GET_TREE_MMTID] = $users_mmtid;
}
else {
$params2[Constants::MM_GET_TREE_INNER_FILTER] = '';
$params2[Constants::MM_GET_TREE_DEPTH] = $mmtid == $users_mmtid ? 0 : 1;
$params2[Constants::MM_GET_TREE_MMTID] = $mmtid;
}
if ($i == $max) {
if ($mmtid != $users_mmtid) $params2[Constants::MM_GET_TREE_DEPTH] = $params[Constants::MM_GET_TREE_DEPTH];
$params2[Constants::MM_GET_TREE_BLOCK] = $params[Constants::MM_GET_TREE_BLOCK];
$params2[Constants::MM_GET_TREE_SORT] = $params[Constants::MM_GET_TREE_DEPTH] != 0 || !empty($params[Constants::MM_GET_TREE_SORT]);
$query[] = mm_content_get_query($params2);
}
else {
$params2[Constants::MM_GET_TREE_BLOCK] = '';
$params2[Constants::MM_GET_TREE_SORT] = FALSE;
$query[] = preg_replace('/ ORDER BY NULL$/', '', mm_content_get_query($params2));
}
}
}
}
// debug_add_dump($mode, $params[Constants::MM_GET_TREE_HERE], Database::getConnection()->prefixTables(join(' UNION ', $query)));
$params['q'] = new GetTreeResults(join(' UNION ', $query), !empty($params[Constants::MM_GET_TREE_FAST]));
$params['q']->level_offset = $params['level'];
return $params['q'];
}
function _mm_content_get_tree_recurs($r, $params, $parent_is_group, $parent_is_user, &$last) {
$_mmtbt_cache = &drupal_static('_mmtbt_cache', []);
$_mmgp_cache = &drupal_static('_mmgp_cache', []);
$_mmuc_cache = &drupal_static('_mmuc_cache', []);
$rows = [];
$last = TRUE;
$xlate = [Constants::MM_PERMS_WRITE, Constants::MM_PERMS_SUB, Constants::MM_PERMS_APPLY, Constants::MM_PERMS_READ, Constants::MM_PERMS_IS_GROUP, Constants::MM_PERMS_IS_USER, Constants::MM_PERMS_ADMIN, Constants::MM_PERMS_IS_RECYCLE_BIN, Constants::MM_PERMS_IS_RECYCLED];
foreach ($xlate as $field) {
if (isset($r->$field)) {
if (!isset($r->perms)) {
$r->perms = [];
}
$r->perms[$field] = $r->$field != 0;
unset($r->$field);
}
}
if (!empty($params[Constants::MM_GET_TREE_RETURN_PERMS]) && !isset($params[Constants::MM_GET_TREE_ITERATOR])) {
if ($r->perms[Constants::MM_PERMS_IS_RECYCLE_BIN]) {
$r->perms[Constants::MM_PERMS_APPLY] = TRUE;
$r->perms[Constants::MM_PERMS_READ] = mm_content_user_can_recycle($r->mmtid, Constants::MM_PERMS_READ, $params[Constants::MM_GET_TREE_USER]);
}
elseif (isset($r->recycle_bins)) {
foreach (explode(',', $r->recycle_bins) as $bin) {
$r->perms[Constants::MM_PERMS_READ] = $r->perms[Constants::MM_PERMS_READ] && mm_content_user_can_recycle($bin, Constants::MM_PERMS_READ, $params[Constants::MM_GET_TREE_USER]);
}
}
}
if (!empty($params[Constants::MM_GET_TREE_ADD_TO_CACHE])) {
if (!isset($_mmtbt_cache[$r->mmtid])) {
$_mmtbt_cache[$r->mmtid] = $r;
}
if (!isset($_mmgp_cache[$r->mmtid])) {
$_mmgp_cache[$r->mmtid] = $r->parent;
}
if (isset($r->perms)) {
foreach ($r->perms as $field => $val) {
if (!isset($_mmuc_cache[$r->mmtid][$params[Constants::MM_GET_TREE_USER]->id()][$field])) {
$_mmuc_cache[$r->mmtid][$params[Constants::MM_GET_TREE_USER]->id()][$field] = $val;
}
}
}
}
if (!empty($params[Constants::MM_GET_TREE_RETURN_FLAGS])) {
$r->flags = _mm_content_split_flags($r->flags);
}
if (!isset($r->is_group)) {
$r->is_group =
$parent_is_group || $r->name == Constants::MM_ENTRY_NAME_GROUPS ||
!empty($params[Constants::MM_GET_TREE_ITERATOR]->parent_is_group) ||
(isset($r->perms) ? $r->perms[Constants::MM_PERMS_IS_GROUP] : mm_content_user_can($r->mmtid, Constants::MM_PERMS_IS_GROUP, $params[Constants::MM_GET_TREE_USER]));
}
if (!isset($r->is_user)) {
$r->is_user =
$parent_is_user || $r->name == Constants::MM_ENTRY_NAME_USERS ||
!empty($params[Constants::MM_GET_TREE_ITERATOR]->parent_is_user) ||
(isset($r->perms) ? $r->perms[Constants::MM_PERMS_IS_USER] : mm_content_user_can($r->mmtid, Constants::MM_PERMS_IS_USER, $params[Constants::MM_GET_TREE_USER]));
}
$r->is_dot = $r->name[0] == '.';
if ($r->is_group) {
unset($r->nodecount);
}
$visible = (!empty($params[Constants::MM_GET_TREE_FILTER_GROUPS]) || !$r->is_group) &&
(!empty($params[Constants::MM_GET_TREE_FILTER_NORMAL]) || $r->is_group || $r->is_user) &&
(!empty($params[Constants::MM_GET_TREE_FILTER_USERS]) || !$r->is_user);
if ($r->is_user && in_array($r->name, \Drupal::state()->get('monster_menus.hidden_user_names', []))) {
$r->bid = Constants::MM_MENU_UNSET;
}
if ($visible || $r->mmtid == 1) {
if ($r->is_group || $r->name == Constants::MM_ENTRY_NAME_USERS || $r->mmtid == 1) {
unset($r->nodecount);
}
$params2 = $params;
if (is_array($params[Constants::MM_GET_TREE_HERE])) {
$params2[Constants::MM_GET_TREE_HERE] =& $params[Constants::MM_GET_TREE_HERE];
}
$params2[Constants::MM_GET_TREE_DEPTH] = $params[Constants::MM_GET_TREE_DEPTH] < 0 ? -1 : $params[Constants::MM_GET_TREE_DEPTH] - 1;
if ($params[Constants::MM_GET_TREE_HERE] && $r->mmtid == $params[Constants::MM_GET_TREE_HERE][0]) {
$r->state = count($params[Constants::MM_GET_TREE_HERE]) >= 2 ? Constants::MM_GET_TREE_STATE_EXPANDED : Constants::MM_GET_TREE_STATE_EXPANDED|Constants::MM_GET_TREE_STATE_HERE;
array_shift($params[Constants::MM_GET_TREE_HERE]);
if ($params[Constants::MM_GET_TREE_BLOCK] && ($r->bid == Constants::MM_MENU_UNSET && $r->max_depth >= 0 || $r->bid != Constants::MM_MENU_UNSET && $r->bid != Constants::MM_MENU_DEFAULT)) {
$depth_new = $r->max_depth;
if ($depth_new == -1) {
$params2[Constants::MM_GET_TREE_DEPTH] = $params[Constants::MM_GET_TREE_DEPTH] = -1;
}
else {
$params[Constants::MM_GET_TREE_DEPTH] = $depth_new;
$params2[Constants::MM_GET_TREE_DEPTH] = $depth_new - 1;
}
}
elseif ($params[Constants::MM_GET_TREE_DEPTH] < 0 || $params[Constants::MM_GET_TREE_DEPTH] > 2) {
$params[Constants::MM_GET_TREE_DEPTH] = count($params[Constants::MM_GET_TREE_HERE]) + 2;
$params2[Constants::MM_GET_TREE_DEPTH] = $params[Constants::MM_GET_TREE_DEPTH] - 1;
}
$params['found'] = $r->mmtid;
if ($params[Constants::MM_GET_TREE_PRUNE_PARENTS] && $r->max_parents != '' && $r->max_parents >= 0) {
$params['pprune'] = $r->max_parents + 2;
}
}
else {
$r->state = $params[Constants::MM_GET_TREE_DEPTH] && $r->parent <= 0 ? Constants::MM_GET_TREE_STATE_EXPANDED : (isset($r->kids) && $r->kids > 0 ? Constants::MM_GET_TREE_STATE_COLLAPSED : Constants::MM_GET_TREE_STATE_LEAF);
if (is_array($params[Constants::MM_GET_TREE_HERE])) {
$params2[Constants::MM_GET_TREE_DEPTH] = 0;
$params2['once'] = TRUE;
foreach ([Constants::MM_GET_TREE_PRUNE_PARENTS, Constants::MM_GET_TREE_RETURN_NODE_COUNT] as $mode) {
$params2[$mode] = FALSE;
}
}
}
if ((!$params[Constants::MM_GET_TREE_BLOCK] || $r->bid == (string) $params[Constants::MM_GET_TREE_BLOCK] || $r->bid == Constants::MM_MENU_UNSET || $r->bid == Constants::MM_MENU_DEFAULT) && (!$r->hidden || !$r->level || $params[Constants::MM_GET_TREE_FILTER_HIDDEN] || isset($params[Constants::MM_GET_TREE_ITERATOR]) || !isset($r->perms) || !empty($r->perms[Constants::MM_PERMS_WRITE]) || !empty($r->perms[Constants::MM_PERMS_SUB]) || !empty($r->perms[Constants::MM_PERMS_APPLY]) || \Drupal::currentUser()->hasPermission('view all menus'))) {
if (!isset($params[Constants::MM_GET_TREE_ITERATOR])) {
$parent = count($rows);
}
if ($r->state) {
if ($r->hidden) {
$r->state |= Constants::MM_GET_TREE_STATE_HIDDEN;
}
elseif ($r->name == Constants::MM_ENTRY_NAME_RECYCLE) {
$r->state |= Constants::MM_GET_TREE_STATE_RECYCLE;
}
if (!$r->is_group) {
if (isset($r->perms[Constants::MM_PERMS_READ]) && !$r->perms[Constants::MM_PERMS_READ]) {
$r->state |= Constants::MM_GET_TREE_STATE_DENIED;
$skip_kids = TRUE;
}
if (empty($r->default_mode)) {
$r->state |= Constants::MM_GET_TREE_STATE_NOT_WORLD;
}
}
if ($visible) {
if (!isset($params[Constants::MM_GET_TREE_ITERATOR])) {
$rows[] = $r;
if (!empty($params['once'])) {
return $rows;
}
}
elseif (!empty($params['once'])) {
return;
}
elseif (($iter_ok = $params[Constants::MM_GET_TREE_ITERATOR]->iterate($r)) < 0) {
$last = FALSE;
$skip_kids = TRUE;
}
elseif (!$iter_ok) {
$last = 'abort';
return;
}
}
}
if (!isset($skip_kids) && $params[Constants::MM_GET_TREE_DEPTH]) {
if (isset($params[Constants::MM_GET_TREE_ITERATOR])) {
$ois_grp = !empty($params[Constants::MM_GET_TREE_ITERATOR]->parent_is_group);
$ois_user = !empty($params[Constants::MM_GET_TREE_ITERATOR]->parent_is_user);
$params[Constants::MM_GET_TREE_ITERATOR]->parent_is_group = $r->is_group;
$params[Constants::MM_GET_TREE_ITERATOR]->parent_is_user = $r->is_user;
}
$params2['found'] = -1;
if (!empty($params2['once'])) $params2['pprune'] = -1;
$params2['level'] = $params['level'] + 1;
$params2['parent_level'] = $r->level;
$kids = _mm_content_get_tree($r->mmtid, $params2);
if ($params2['pprune'] >= 0) {
if ($params2['pprune'] == 0) {
$params['pprune'] = 0;
return $kids;
}
elseif ($params2['found']) {
$params['pprune'] = $params2['pprune'];
}
}
if (isset($params[Constants::MM_GET_TREE_ITERATOR])) {
$params[Constants::MM_GET_TREE_ITERATOR]->parent_is_group = $ois_grp;
$params[Constants::MM_GET_TREE_ITERATOR]->parent_is_user = $ois_user;
if (!empty($params2['abort'])) {
$last = 'abort';
return $rows;
}
}
else {
if (count($rows) > $parent) {
if ($rows[$parent]->is_group) {
foreach ($kids as $k) {
$k->is_group = TRUE;
unset($k->nodecount);
}
}
if ($rows[$parent]->is_user) {
foreach ($kids as $k) {
$k->is_user = TRUE;
$k->is_user_home = $k->level == $rows[$parent]->level + 1 && $rows[$parent]->name == Constants::MM_ENTRY_NAME_USERS;
}
}
if ($params['found'] != $r->mmtid) {
$rows[$parent]->state &= ~(Constants::MM_GET_TREE_STATE_EXPANDED|Constants::MM_GET_TREE_STATE_COLLAPSED|Constants::MM_GET_TREE_STATE_LEAF);
$rows[$parent]->state |= $params2['found'] >= 0 ? Constants::MM_GET_TREE_STATE_EXPANDED :
(count($kids) || isset($r->kids) && $r->kids > 0 ? Constants::MM_GET_TREE_STATE_COLLAPSED : Constants::MM_GET_TREE_STATE_LEAF);
}
}
if ((!isset($params['once']) || !$params['once']) && is_array($kids)) {
$rows = array_merge($rows, $kids);
}
}
if ($params2['found'] >= 0) {
$params['found'] = $params2['found'];
}
} // if( $params[Constants::MM_GET_TREE_DEPTH] )
else {
$skip_kids = TRUE;
}
} // if( !$params[Constants::MM_GET_TREE_BLOCK] || ...
else {
$skip_kids = TRUE;
}
} // if( $visible || $r->parent<=0 )
else {
$skip_kids = TRUE;
}
if (isset($skip_kids)) {
/** @noinspection PhpStatementHasEmptyBodyInspection */
while (($row = $params['q']->next()) && strlen($row->sort_idx) > strlen($r->sort_idx));
if ($row) {
$params['q']->back();
}
}
$last = FALSE;
return $rows;
}
/**
* Get the cascaded (inherited by children) settings for an entry.
*
* This function returns the exact settings for a particular entry, and does not
* consider the settings of its parents. To include the parents' settings, see
* mm_content_resolve_cascaded_setting().
*
* @param int $mmtid
* ID of the entry to load settings for. If NULL, return a list of possible
* settings and their data representation. The structure of the returned array
* in this case is:
* - data_type: 'int' (integer) or 'string'
* - multiple: TRUE if multiple values are accepted
* - user_access: user must have $account->hasPermission() for this value in
* order to set the setting
* - not_empty: TRUE if only !empty() values should be stored
* @param string|null $name
* Name of the setting to return, or NULL to return all settings
* @return mixed
* Either an array or a single value, depending on $name
*/
function mm_content_get_cascaded_settings($mmtid = NULL, $name = NULL) {
static $drupal_static_fast;
if (is_null($mmtid)) {
// $drupal_static_fast['settings'] should never be empty after it has been
// set once, but for some reason it sometimes is during tests.
if (!isset($drupal_static_fast) || empty($drupal_static_fast['settings'])) {
// This is cumbersome, but assigning to an array is the only way that works.
$drupal_static_fast['settings'] = &drupal_static(__FUNCTION__);
// Check for mm_cascaded_settings hooks
$drupal_static_fast['settings'] = \Drupal::moduleHandler()->invokeAll('mm_cascaded_settings');
}
return $drupal_static_fast['settings'];
}
$cascaded = [];
$result = Database::getConnection()->select('mm_cascaded_settings', 's')
->fields('s')
->condition('s.mmtid', $mmtid)
->execute();
foreach ($result as $r) {
if ($r->data_type == 'int') {
$r->data = (int) $r->data;
}
if ($r->multiple) {
if (!isset($cascaded[$r->name]) || !is_array($cascaded[$r->name])) {
$cascaded[$r->name] = [];
}
if ($r->array_key != '') {
$cascaded[$r->name][$r->array_key] = $r->data;
}
else {
$cascaded[$r->name][] = $r->data;
}
}
else {
$cascaded[$r->name] = $r->data;
}
}
if (!empty($name)) {
if (isset($cascaded[$name])) {
return $cascaded[$name];
}
$settings = mm_content_get_cascaded_settings();
return empty($settings[$name]['multiple']) ? NULL : [];
}
return $cascaded;
}
/**
* Set the cascaded (inherited by children) settings for an entry
*
* @param int $mmtid
* Tree ID of the entry to set settings for
* @param array $settings
* Array containing the settings
* @param bool $delete
* If TRUE, delete all old settings for $mmtid first
* @param Connection $database
* (optional) The database connection to use.
*/
function mm_content_set_cascaded_settings($mmtid, $settings, $delete = TRUE, Connection $database = NULL) {
$cascaded_settings = mm_content_get_cascaded_settings();
$database = $database ?: Database::getConnection();
$insert = function($mmtid, $name, $desc, $array_key, $data) use ($database) {
if ($desc['data_type'] == 'int') {
if ($data === '' || ($data = intval($data)) == -1) {
return;
}
}
elseif (!empty($desc['not_empty']) && empty($data)) {
return;
}
if (!isset($desc['use_keys']) || !$desc['use_keys']) {
$array_key = '';
}
$database->insert('mm_cascaded_settings')
->fields(['mmtid' => $mmtid, 'name' => $name, 'data_type' => $desc['data_type'], 'multiple' => empty($desc['multiple']) ? 0 : 1, 'array_key' => $array_key, 'data' => $data])
->execute();
};
if ($delete) {
$database->delete('mm_cascaded_settings')
->condition('mmtid', $mmtid)
->execute();
mm_content_notify_change('clear_cascaded', $mmtid, NULL);
}
foreach ($cascaded_settings as $name => $desc) {
if (isset($settings[$name])) {
if (!empty($desc['multiple'])) {
foreach ($settings[$name] as $array_key => $data) {
$insert($mmtid, $name, $desc, $array_key, $data);
}
}
else {
$insert($mmtid, $name, $desc, '', $settings[$name]);
}
}
}
if ($added = array_intersect_key($settings, $cascaded_settings)) {
mm_content_notify_change('insert_cascaded', $mmtid, NULL, $added);
}
}
/**
* Notify hook_mm_notify_change() implementations that a change has occurred in
* one or more nodes or MM pages.
*
* @param string $type
* A string representing the type of change that occurred:
* - 'clear_cascaded':
* All cascaded settings have been cleared for the tree entries.
* - 'clear_flags':
* All flags have been cleared for the tree entries.
* - 'delete_node':
* The nodes with $nids, described by $data, have been permanently deleted.
* - 'delete_page':
* The tree entries with $mmtids have been permanently deleted.
* - 'insert_cascaded':
* One or more cascaded settings were added to the tree entries.
* - 'insert_flags':
* One or more flags were added to the tree entries.
* - 'insert_node':
* A node has been created. $data describes it.
* - 'insert_page':
* A tree entry has been created. $data describes it.
* - 'move_node':
* The nodes described by $nids have moved from $data['old_mmtid'] to
* $data['new_mmtid'].
* - 'move_page':
* The tree entries at $mmtids have moved from $data['old_parent'] to
* $data['new_parent'].
* - 'update_node':
* A node has been updated. $data describes the entire new state.
* - 'update_node_perms':
* The nodes' permissions have been modified to match $data.
* - 'update_page':
* A tree entry has been updated. $data describes the entire new state.
* - 'update_page_quick':
* A portion of the tree entry's settings have changed, according to $data.
* @param int|array|null $mmtids
* A single tree ID or an array of IDs that were affected, or NULL if none
* @param int|array|null $nids
* A single node ID or an array of IDs that were affected, or NULL if none
* @param mixed $data
* A $type-specific description of the change
*/
function mm_content_notify_change($type, $mmtids = NULL, $nids = NULL, mixed $data = NULL) {
if (isset($nids) && !is_array($nids)) {
$nids = [$nids];
}
else if (empty($nids)) {
$nids = [];
}
if (isset($mmtids) && !is_array($mmtids)) {
$mmtids = [$mmtids];
}
else if (empty($mmtids)) {
$mmtids = [];
}
\Drupal::moduleHandler()->invokeAll('mm_notify_change', [$type, $mmtids, $nids, $data]);
}
/**
* Scan a tree entry and its parents upward, looking for the closest change in a
* cascaded setting.
*
* To retrieve the settings for a particular entry without considering its
* parents, see mm_content_get_cascaded_settings().
*
* @param string $name
* Setting to look for
* @param int $mmtid
* Tree ID of the entry (and its parents) to query
* @param int $at
* Tree ID where the closest change occurs
* @param int $parent
* Tree ID of the nearest parent after $at containing a change in state
* @param bool $new_entry
* Set to TRUE if $mmtid is that of the (future) parent of a new child
* @return mixed
* An array or single value (depending on the data type) containing the state
* of the given settings at the level $at
*/
function mm_content_resolve_cascaded_setting($name, $mmtid, &$at, &$parent, $new_entry = FALSE) {
$q = Database::getConnection()->query('SELECT s.* FROM (SELECT :mmtid1 AS mmtid, 10000 AS depth UNION SELECT parent, depth FROM {mm_tree_parents} WHERE mmtid = :mmtid2) t INNER JOIN {mm_cascaded_settings} s ON s.mmtid = t.mmtid WHERE s.name = :name ORDER BY t.depth DESC',
[':mmtid1' => $mmtid, ':mmtid2' => $mmtid, ':name' => $name]
);
$out = [];
$r = $q->fetch();
while ($r) {
$this_mmtid = $r->mmtid;
if (is_array($out) && !$out) {
if ($r->multiple) {
do {
if ($r->data_type == 'int') {
$r->data = (int) $r->data;
}
if ($r->array_key != '') {
$out[$r->array_key] = $r->data;
}
else {
$out[] = $r->data;
}
$r = $q->fetch();
} while ($r && $r->multiple && $r->mmtid == $this_mmtid);
}
else {
if ($r->data_type == 'int') {
$r->data = (int) $r->data;
}
$out = $r->data;
}
$at = $this_mmtid;
}
elseif ($r->multiple) {
do {
$r = $q->fetch();
} while ($r && $r->multiple && $r->mmtid == $this_mmtid);
}
else {
$r = $q->fetch();
}
if ($new_entry || $this_mmtid != $mmtid) {
$parent = $this_mmtid;
return $out;
}
}
$parent = 0;
if (!$out && $out !== 0) {
$cascaded_settings = mm_content_get_cascaded_settings();
if (!isset($cascaded_settings[$name]['multiple']) || !$cascaded_settings[$name]['multiple']) return NULL;
}
return $out;
}
/**
* Get the parent tree ID of an entry
*
* @param int|array $mmtids
* Tree ID (or array of Tree IDs) of the entry whose parent we are looking for
* @return int|array|null
* Tree ID of the parent (if a single $mmtid is supplied), or an array where
* the key is the child Tree ID and the value is the parent
*/
function mm_content_get_parent($mmtids) {
if (is_array($mmtids)) {
if (count($mmtids) == 1) {
// If only one mmtid, use the simple case because it might be cached.
$mmtid = array_pop($mmtids);
$parent = mm_content_get_parent($mmtid);
if (isset($parent)) {
return [$mmtid => $parent];
}
return [];
}
return Database::getConnection()->select('mm_tree', 't')
->fields('t', ['mmtid', 'parent'])
->condition('mmtid', $mmtids, 'IN')
->execute()
->fetchAllKeyed();
}
$t = mm_content_get($mmtids);
if ($t) {
return $t->parent;
}
return NULL;
}
/**
* Get all parent tree IDs of an entry
*
* @param int $mmtid
* Tree ID of the entry whose parent we are looking for
* @param bool $slow
* If TRUE, don't rely on the 'parents' field of the mm_tree table, instead
* slowly traverse up the tree
* @param bool $virtual
* If TRUE, include the negative IDs that are added to children of the /.Users
* entry by mm_content_get_tree().
* @return array
* Array of parent tree IDs, listed highest-first
*/
function mm_content_get_parents($mmtid, $slow = FALSE, $virtual = TRUE) {
$_mmtbt_cache = &drupal_static('_mmtbt_cache', []);
$_mmgp_cache = &drupal_static('_mmgp_cache', []);
$list = [];
$mmtid0 = $mmtid;
if ($mmtid < 0) {
return [1, mm_content_users_mmtid()];
}
if (!$slow) {
if ($mmtid == 1) {
return $list;
}
while ($mmtid > 1 && isset($_mmgp_cache[$mmtid])) {
array_unshift($list, $mmtid = $_mmgp_cache[$mmtid]);
}
if ($mmtid > 1) {
$r = mm_content_get($mmtid, Constants::MM_GET_PARENTS);
if (empty($r)) {
return $list;
}
$list = array_merge($r->parents, $list);
$prev = $mmtid0;
foreach (array_reverse($list) as $m) {
$_mmgp_cache[$prev] = $m;
$prev = $m;
}
}
}
else {
$last = -1;
do {
$mmtid = mm_content_get_parent($mmtid);
if ($mmtid) {
array_unshift($list, $mmtid);
}
if ($mmtid == $last) {
// shouldn't happen, but just in case
break;
}
$last = $mmtid;
}
while ($mmtid > 1);
}
$virtual = $virtual && mm_get_setting('user_homepages.virtual');
if ($virtual && count($list) >= 2 && $list[1] == mm_content_users_mmtid()) {
$m = count($list) >= 3 ? $list[2] : $mmtid0;
$tree = $_mmtbt_cache[$m] ?? mm_content_get($m);
if ($tree) {
$letr = mb_strtoupper($tree->name[0]);
$alias = ctype_alpha($letr) ? $letr : '~';
array_splice($list, 2, 0, -ord($alias));
}
}
return $list;
}
/**
* Get all parent tree IDs of an entry, plus the ID itself
*
* @param int $mmtid
* Tree ID of the entry whose parent we are looking for
* @param bool $slow
* If TRUE, don't rely on the 'parents' field of the mm_tree table, instead
* slowly traverse up the tree
* @param bool $virtual
* If TRUE, include the negative IDs that are added to children of the
* /.Users entry by mm_content_get_tree().
* @return array
* Array of parent tree IDs, listed highest-first, with $mmtid at the end
*/
function mm_content_get_parents_with_self($mmtid, $slow = FALSE, $virtual = TRUE) {
$list = mm_content_get_parents($mmtid, $slow, $virtual);
$list[] = $mmtid;
return $list;
}
/**
* Get the full tree path of a tree ID
*
* @param int $mmtid
* Tree ID of the page whose path we are looking for
* @return string
* Full path in the format 1/7/234/847
*/
function mm_content_get_full_path($mmtid) {
return join('/', mm_content_get_parents_with_self($mmtid));
}
/**
* Get a page's name.
*
* @param int|object $mmtid_or_tree
* Tree ID or full tree object of the page whose name is being requested. If
* known, it is better to provide the full object, to avoid extra queries to
* the database.
* @return string
* The expanded name
* @see hook_mm_mmtid_name
*/
function mm_content_get_name($mmtid_or_tree) {
static $drupal_static_fast;
if (!is_object($mmtid_or_tree)) {
if (!($mmtid_or_tree = mm_content_get($mmtid_or_tree))) {
return '';
}
}
if (!isset($drupal_static_fast)) {
// This is cumbersome, but assigning to an array is the only way that works.
$drupal_static_fast['table'] = &drupal_static(__FUNCTION__, []);
// We can't use (mm_)module_invoke_all() as it does not handle numeric keys
// (mmtids, in this case) correctly.
foreach (mm_module_implements('mm_mmtid_name') as $function) {
$drupal_static_fast['table'] += $function();
}
}
$mmtid = $mmtid_or_tree->mmtid;
if (empty($mmtid_or_tree->name)) {
$mmtid_or_tree->name = t('Undefined @number', ['@number' => $mmtid]);
}
$table = &$drupal_static_fast['table'];
if (isset($table[$mmtid])) {
if (is_array($table[$mmtid])) {
if (isset($table[$mmtid]['callback']) && function_exists($table[$mmtid]['callback'])) {
$result = call_user_func($table[$mmtid]['callback'], $mmtid_or_tree);
if (!empty($result)) {
return $result;
}
}
if (!empty($table[$mmtid]['name'])) {
return $table[$mmtid]['name'];
}
}
else {
return $table[$mmtid];
}
}
return mm_content_expand_name($mmtid_or_tree->name);
}
/**
* Get a list of tree entries, using their tree IDs or other attributes.
*
* @param mixed $options
* Either a single tree ID, an array of tree IDs, or an associative array
* containing key => value pairs of attributes to query. When using an
* associative array, the value can be an array of values. The allowed keys
* are all the columns in the mm_tree table, plus:
* - query: a sub-query which returns a list of mmtids to query against
* - flags: an array of key => value pairs which are ANDed together; a NULL
* value becomes IS NULL in the query
* @param array|string $return
* A single value, or an array of values, from the list of constants below:
* - MM_GET_ARCHIVE: return archive status (mm_archive)
* - MM_GET_FLAGS: return flags (mm_tree_flags)
* - MM_GET_PARENTS: return parents (mm_tree_parents)
* @param int $limit
* Optional maximum number of results to return (0)
* @param bool $sort
* If TRUE, sort the results by their position in the tree (FALSE)
* @return array|object
* If $options['mmtids'] is a single tree ID, return the one tree object.
* Otherwise, return an array of tree objects (order is random).
*/
function mm_content_get(mixed $options, $return = [], $limit = 0, $sort = FALSE) {
$_mmtbt_cache = &drupal_static('_mmtbt_cache', []);
$single = FALSE;
if (!is_array($options)) {
$single = TRUE;
$options = ['mmtid' => [$options]];
}
elseif (is_numeric(mm_ui_mmlist_key0($options))) {
$options = ['mmtid' => $options];
}
if (!is_array($return)) {
$return = [$return];
}
$return = array_flip($return);
$out = $args = $wheres = $joins = [];
$add_field = $group_by = '';
// Use a cache in the simple case where the only keys are mmtids
if (isset($options['mmtid']) && count(array_keys($options)) == 1 && !$sort) {
if (!is_array($options['mmtid'])) {
$options['mmtid'] = [$options['mmtid']];
}
foreach ($options['mmtid'] as $key => $mmtid) {
if (isset($_mmtbt_cache[$mmtid]) && (!isset($return[Constants::MM_GET_ARCHIVE]) || isset($_mmtbt_cache[$mmtid]->archive_cached)) && (!isset($return[Constants::MM_GET_FLAGS]) || isset($_mmtbt_cache[$mmtid]->flags)) && (!isset($return[Constants::MM_GET_PARENTS]) || isset($_mmtbt_cache[$mmtid]->parents))) {
if (!$limit || count($out) < $limit) {
$out[] = clone $_mmtbt_cache[$mmtid];
}
unset($options['mmtid'][$key]);
}
elseif ($mmtid < 0) {
if (!$limit || count($out) < $limit) {
$out[] = _mm_content_virtual_dir($mmtid, mm_content_users_mmtid(), 0, 0);
}
unset($options['mmtid'][$key]);
}
elseif (!is_numeric($mmtid) || !$mmtid) {
unset($options['mmtid'][$key]);
}
}
// Reset array keys after unset()
$options['mmtid'] = array_merge($options['mmtid']);
}
if (isset($options['query']) && !isset($options['mmtid'])) {
$wheres[] = 't.mmtid IN (' . $options['query'] . ')';
$single = FALSE;
unset($options['query']);
}
if (isset($options['flags']) && is_array($options['flags'])) {
$n = 0;
foreach ($options['flags'] as $flag => $data) {
$joins[] = "LEFT JOIN {mm_tree_flags} f$n ON f$n.mmtid = t.mmtid";
$wheres[] = "f$n.flag = :ff$n";
$args[":ff$n"] = $flag;
if (is_null($data)) {
$wheres[] = "f$n.data IS NULL";
}
else {
$wheres[] = "f$n.data = :fd$n";
$args[":fd$n"] = $data;
}
$n++;
}
$single = FALSE;
unset($options['flags']);
}
$n = 0;
foreach ($options as $k => $v) {
if (!is_array($v) || $v) {
if (is_array($v) && count($v) == 1) {
$v = $v[0];
}
$k = strtolower($k);
if (strchr($k, '.') === FALSE) {
$k = "t.$k";
}
if (is_array($v)) {
$vals = [];
foreach ($v as $v2) {
$vals[] = ":v$n";
$args[":v$n"] = $v2;
$n++;
}
$wheres[] = "$k IN(" . join(', ', $vals) . ')';
}
else {
$wheres[] = "$k = :v$n";
$args[":v$n"] = $v;
$n++;
}
}
}
if ($limit) {
// Consider cached data already copied to $out
$limit -= count($out);
if ($limit <= 0) {
return $single ? $out[0] : $out;
}
}
if (isset($return[Constants::MM_GET_FLAGS])) {
$joins[] = 'LEFT JOIN {mm_tree_flags} f ON f.mmtid = t.mmtid';
$add_field .= ", GROUP_CONCAT(DISTINCT CONCAT_WS('|1', f.flag, f.data) SEPARATOR '|2') AS flags";
$group_by = ' GROUP BY t.mmtid';
}
if (isset($return[Constants::MM_GET_PARENTS])) {
$joins[] = 'LEFT JOIN {mm_tree_parents} p ON p.mmtid = t.mmtid';
$add_field .= ', GROUP_CONCAT(DISTINCT p.parent ORDER BY p.depth) AS parents';
$group_by = ' GROUP BY t.mmtid';
}
if (isset($return[Constants::MM_GET_ARCHIVE])) {
$joins[] = 'LEFT JOIN {mm_archive} a ON a.main_mmtid = t.mmtid OR a.archive_mmtid = t.mmtid';
$add_field .= ', ' . ($group_by ? mm_all_db_columns('mm_archive', [], 'a.') : 'a.*') . ', 1 AS archive_cached';
}
if (!empty($wheres)) {
$space = count($joins) ? ' ' : '';
$query = 'SELECT ' . ($group_by ? mm_all_db_columns('mm_tree', ['mmtid'], 't.') : 't.*') . "$add_field FROM {mm_tree} t$space" . join(' ', $joins) . ' WHERE ' . join(' AND ', $wheres) . $group_by;
if ($sort) {
$query .= ' ORDER BY t.sort_idx';
}
if ($limit) {
$q = Database::getConnection()->queryRange($query, 0, $limit, $args);
}
else {
$q = Database::getConnection()->query($query, $args);
}
foreach ($q as $r) {
if (isset($return[Constants::MM_GET_FLAGS])) {
$r->flags = _mm_content_split_flags($r->flags);
}
elseif (isset($_mmtbt_cache[$r->mmtid], $_mmtbt_cache[$r->mmtid]->flags)) {
$r->flags = $_mmtbt_cache[$r->mmtid]->flags;
}
if (isset($return[Constants::MM_GET_PARENTS]) && !is_array($r->parents)) {
$r->parents = empty($r->parents) ? [] : explode(',', $r->parents);
}
elseif (isset($_mmtbt_cache[$r->mmtid], $_mmtbt_cache[$r->mmtid]->parents)) {
$r->parents = $_mmtbt_cache[$r->mmtid]->parents;
}
if (!isset($return[Constants::MM_GET_ARCHIVE]) && isset($_mmtbt_cache[$r->mmtid], $_mmtbt_cache[$r->mmtid]->archive_cached)) {
$r->archive_cached = 1;
$r->main_mmtid = $_mmtbt_cache[$r->mmtid]->main_mmtid;
$r->archive_mmtid = $_mmtbt_cache[$r->mmtid]->archive_mmtid;
$r->frequency = $_mmtbt_cache[$r->mmtid]->frequency;
$r->main_nodes = $_mmtbt_cache[$r->mmtid]->main_nodes;
}
$_mmtbt_cache[$r->mmtid] = $out[] = $r;
}
}
return $single && isset($out[0]) ? $out[0] : $out;
}
/**
* Update a tree entry's list of parent nodes, or update the lists for all
* entries in the tree.
*
* @param int $mmtid
* ID of the entry to update, or NULL to update all entries
* @param array|null $parents
* Array of parent IDs, or NULL to recalculate from the tree
* @param bool $is_new
* Set to TRUE if the entry doesn't already have parents, to avoid an extra
* DELETE
* @param bool $force
* When $mmtid is NULL, set this parameter to TRUE in order to force all
* entries to be updated, not just those that currently have no 'parents' info
*/
function mm_content_update_parents($mmtid = NULL, $parents = NULL, $is_new = FALSE, $force = FALSE) {
$db = Database::getConnection();
if (is_null($mmtid)) {
$select = $db->select('mm_tree', 't')
->fields('t', ['mmtid']);
if ($force) {
// SELECT t.mmtid FROM {mm_tree} t
// LEFT JOIN {mm_tree_parents} p ON p.mmtid = t.mmtid
// WHERE t.parent > 0 AND p.parent IS NULL
$select->leftJoin('mm_tree_parents', 'p', 'p.mmtid = t.mmtid');
$select->condition('t.parent', 0, '>')
->isNull('p.parent');
}
$result = $select->execute();
foreach ($result as $r) {
mm_content_update_parents($r->mmtid, NULL);
}
return;
}
$txn = $db->startTransaction(); // Lock DB.
if (is_null($parents)) {
$parents = mm_content_get_parents($mmtid, TRUE, FALSE);
}
if ($parents && !$is_new) {
_mm_content_clear_access_cache($mmtid);
}
$db->delete('mm_tree_parents')
->condition('mmtid', $mmtid)
->execute();
if ($parents) {
if (!$is_new) {
// UPDATE {mm_tree} SET parent = <last parent> WHERE mmtid = <mmtid>
$db->update('mm_tree')
->fields(['parent' => $parents[count($parents) - 1]])
->condition('mmtid', $mmtid)
->execute();
}
$insert = $db->insert('mm_tree_parents')
->fields(['mmtid', 'parent', 'depth']);
foreach ($parents as $depth => $parent) {
$insert->values(['mmtid' => $mmtid, 'parent' => $parent, 'depth' => $depth]);
}
$insert->execute();
}
// Update the variable which controls how many nested joins are performed when
// rewriting inbound URLs.
$max_depth = $db->query('SELECT MAX(depth) FROM {mm_tree_parents}')->fetchField() + 1;
$new_max = min($max_depth, Constants::MM_CONTENT_MYSQL_MAX_JOINS);
if (\Drupal::state()->get('monster_menus.mysql_max_joins', Constants::MM_CONTENT_MYSQL_MAX_JOINS) !== $new_max) {
\Drupal::state()->set('monster_menus.mysql_max_joins', $new_max);
}
mm_module_invoke_all('mm_content_update_parents', $mmtid, $parents, $is_new);
}
/**
* Return the Url for a tree entry
*
* @param int $mmtid
* ID of the entry
* @see \Drupal\Core\Url::fromUri() for details.
* @return Url
* A new Url object for a routed (internal to Drupal) URL.
*/
function mm_content_get_mmtid_url($mmtid, array $options = []) {
return Url::fromRoute('entity.mm_tree.canonical', ['mm_tree' => $mmtid], $options);
}
/**
* Return a list of tree IDs to which a given node is assigned
*
* @param int|array|null $nids
* Node ID or array of node IDs to query
* @param bool $reset
* If TRUE, clear the cache
* @param Connection $database
* (optional) The database connection to use.
* @return array|void
* If $nids is a single nid, return an array of tree IDs, otherwise return an
* outer array keyed by nid, where each value is an array of mmtids.
*/
function mm_content_get_by_nid($nids, $reset = FALSE, Connection $database = NULL) {
if ($reset) {
drupal_static(__FUNCTION__, [], TRUE);
return;
}
$database = $database ?: Database::getConnection();
$db_key = $database->getKey();
$cache = &drupal_static(__FUNCTION__, []);
if (!isset($cache[$db_key])) {
$cache[$db_key] = [];
}
$want_array = TRUE;
if (!is_array($nids)) {
$want_array = FALSE;
$nids = [$nids];
}
$needed = array_diff($nids, array_keys($cache[$db_key]));
if ($needed) {
$result = $database->select('mm_node2tree', 't')
->fields('t', ['mmtid', 'nid'])
->condition('t.nid', $needed, 'IN')
->execute();
foreach ($result as $r) {
$cache[$db_key][$r->nid][] = $r->mmtid;
}
}
if (!$want_array) {
return $cache[$db_key][$nids[0]] ?? [];
}
return array_intersect_key($cache[$db_key], array_flip($nids));
}
/**
* Figure out if a user can see or delete a recycle bin
*
* @param int $mmtid
* ID of the bin being queried
* @param string $mode
* If set, return whether the user can perform that action
* (MM_PERMS_READ (see), MM_PERMS_WRITE (delete)). Otherwise, return an array
* containing these elements with either TRUE or FALSE values. There is also a
* special mode, MM_PERMS_IS_EMPTYABLE, which returns TRUE if the user has
* permission to empty the entire bin (i.e.: has write on everything in it.)
* @param AccountInterface $usr
* User object of the user to test, or NULL to test the current user.
* @return mixed
* See above
*/
function mm_content_user_can_recycle($mmtid, $mode = '', AccountInterface $usr = NULL) {
$user = \Drupal::currentUser();
$_mmucr_cache = &drupal_static('_mmucr_cache', []);
if (!$usr) {
$usr = $user;
}
$uid = $usr->id();
if ($mode != '' ? !isset($_mmucr_cache[$mmtid][$uid][$mode]) : !isset($_mmucr_cache[$mmtid][$uid])) {
$iter = new ContentUserCanRecycleIter($mode, $uid == $user->id() ? NULL : $usr);
if (mm_content_user_can(mm_content_get_parent($mmtid), Constants::MM_PERMS_READ, $usr)) {
$params = [
Constants::MM_GET_TREE_FAKE_READ_BINS => TRUE,
Constants::MM_GET_TREE_USER => $usr,
Constants::MM_GET_TREE_RETURN_PERMS => TRUE,
Constants::MM_GET_TREE_DEPTH => 1,
Constants::MM_GET_TREE_ITERATOR => $iter,
];
mm_content_get_tree($mmtid, $params);
}
elseif ($mode == Constants::MM_PERMS_IS_EMPTYABLE) {
$iter->emptyable = FALSE;
}
if ($mode == Constants::MM_PERMS_IS_EMPTYABLE) {
$_mmucr_cache[$mmtid][$uid][Constants::MM_PERMS_IS_EMPTYABLE] = $iter->emptyable && $usr->hasPermission('delete permanently');
}
else {
$_mmucr_cache[$mmtid][$uid][Constants::MM_PERMS_READ] = $iter->readable;
$_mmucr_cache[$mmtid][$uid][Constants::MM_PERMS_WRITE] = $iter->writable;
}
} // !isset($_mmucr_cache[$mmtid][$uid])
if (mm_site_is_disabled($usr)) {
if ($mode != '') {
return FALSE;
}
return [Constants::MM_PERMS_READ => FALSE, Constants::MM_PERMS_WRITE => FALSE];
}
if ($mode != '') {
return $_mmucr_cache[$mmtid][$uid][$mode] ?? FALSE;
}
return $_mmucr_cache[$mmtid][$uid];
}
/**
* Figure out if a given user can access a particular tree ID
*
* @param int $mmtid
* ID of the term being queried
* @param string $mode
* If set, return whether the user can perform that action
* (MM_PERMS_READ, MM_PERMS_WRITE, MM_PERMS_SUB, MM_PERMS_APPLY,
* MM_PERMS_IS_USER, MM_PERMS_IS_GROUP, MM_PERMS_IS_RECYCLE_BIN,
* MM_PERMS_IS_RECYCLED). Otherwise, return an array containing each of these
* permissions with either TRUE or FALSE values.
* @param AccountInterface $usr
* User object to test against. Defaults to the current user.
* @param bool $bias_anon
* If TRUE, assume user 0 can't read any groups (faster, more secure)
* @return bool|array
* See above
*/
function mm_content_user_can($mmtid, $mode = '', AccountInterface $usr = NULL, $bias_anon = TRUE) {
$_mmuc_cache = &drupal_static('_mmuc_cache', []);
if (!$usr) $usr = \Drupal::currentUser();
$uid = $usr->id();
$mmtid = intval($mmtid);
if (!empty($mode) ? !isset($_mmuc_cache[$mmtid][$uid][$mode]) : !isset($_mmuc_cache[$mmtid][$uid])) {
// set default values, in case mmtid does not exist
$_mmuc_cache[$mmtid][$uid] = [
Constants::MM_PERMS_WRITE => FALSE,
Constants::MM_PERMS_SUB => FALSE,
Constants::MM_PERMS_APPLY => FALSE,
Constants::MM_PERMS_READ => FALSE,
Constants::MM_PERMS_IS_USER => FALSE,
Constants::MM_PERMS_IS_GROUP => FALSE,
Constants::MM_PERMS_IS_RECYCLE_BIN => FALSE,
Constants::MM_PERMS_IS_RECYCLED => FALSE,
];
if ($mmtid < 0) {
// speedup for virtual user directory (A-Z)
$_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_READ] = $_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_IS_USER] = TRUE;
}
elseif ($mmtid) {
$cid = "$mmtid::$uid";
$cached = _mm_content_access_cache($cid);
if (is_array($cached)) {
$_mmuc_cache[$mmtid][$uid] = $cached;
}
else {
$params = [
Constants::MM_GET_TREE_BIAS_ANON => $bias_anon,
Constants::MM_GET_TREE_FAKE_READ_BINS => TRUE,
Constants::MM_GET_TREE_MMTID => $mmtid,
Constants::MM_GET_TREE_RETURN_BINS => TRUE,
Constants::MM_GET_TREE_RETURN_PERMS => TRUE,
Constants::MM_GET_TREE_USER => $usr,
];
$row = Database::getConnection()->query(mm_content_get_query($params))->fetchObject();
if ($row) {
$bins = [];
foreach ((array)$row as $key => $val) {
if ($key == 'recycle_bins') {
if (!empty($val)) $bins = explode(',', $val);
}
else {
$_mmuc_cache[$mmtid][$uid][$key] = $val != 0;
}
}
// it's too expensive to do this in the query
if ($_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_IS_RECYCLE_BIN]) {
$_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_APPLY] = TRUE;
$_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_READ] = mm_content_user_can_recycle($mmtid, Constants::MM_PERMS_READ, $usr);
}
else {
// re-calculate the MM_PERMS_READ flag for anything in a bin
foreach ($bins as $bin) {
$_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_READ] = $_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_READ] && mm_content_user_can_recycle($bin, Constants::MM_PERMS_READ, $usr);
}
}
}
_mm_content_access_cache($cid, $_mmuc_cache[$mmtid][$uid], $uid, 0, $mmtid);
}
}
}
if (mm_site_is_disabled($usr)) {
$_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_WRITE] = $_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_APPLY] = $_mmuc_cache[$mmtid][$uid][Constants::MM_PERMS_SUB] = FALSE;
}
if (!empty($mode)) {
return $_mmuc_cache[$mmtid][$uid][$mode];
}
return $_mmuc_cache[$mmtid][$uid];
}
/**
* Get a database query to return a part of the tree, or to determine whether or
* not a user has permission to access a particular node or part of the tree
*
* @param array $params
* An array containing parameters. The array is indexed using the constants
* below. Either [MM_GET_TREE_NODE] or [MM_GET_TREE_MMTID] must be specified.
* - MM_GET_TREE_ADD_SELECT (none):
* A string or array of strings to add to the SELECT portion of the query
* - MM_GET_TREE_BIAS_ANON (TRUE):
* If TRUE, assume user 0 can't read any groups (more secure)
* - MM_GET_TREE_DEPTH (0):
* When 'mmtid' is used, a query to return all items in the tree below that
* point can be returned. This field specifies the depth of recursion:
* - 0: just the item specified by MM_GET_TREE_MMTID
* - -1: all levels
* - 1: the item and its immediate children
* - N: any other other number will return that many levels (can be slow)
* - MM_GET_TREE_FAKE_READ_BINS (FALSE):
* Pretend the user can read all recycle bins (used internally)
* - MM_GET_TREE_FILTER_BINS (TRUE):
* Get entries that are recycle bins
* - MM_GET_TREE_FILTER_DOTS (TRUE):
* Get all entries with names that start with '.'. If FALSE, only .Groups,
* .Users, and .Virtual are returned.
* - MM_GET_TREE_FILTER_GROUPS (TRUE):
* Get entries that are groups (MM_GET_TREE_MMTID mode)
* - MM_GET_TREE_FILTER_NORMAL (TRUE):
* Get entries that are neither groups nor in /users (MM_GET_TREE_MMTID
* mode)
* - MM_GET_TREE_FILTER_USERS (TRUE):
* Get entries in /users (MM_GET_TREE_MMTID mode)
* - MM_GET_TREE_INNER_FILTER:
* Used internally
* - MM_GET_TREE_MMTID:
* Tree ID to query
* - MM_GET_TREE_NODE:
* NodeInterface object to query permissions for
* - MM_GET_TREE_RETURN_BINS (FALSE):
* A comma-separated list of the mmtids of any parent recycle bins
* - MM_GET_TREE_RETURN_BLOCK (FALSE):
* Attributes from the mm_tree_block table (MM_GET_TREE_MMTID mode)
* - MM_GET_TREE_RETURN_FLAGS (FALSE):
* Flags from the mm_tree_flags table (MM_GET_TREE_MMTID mode)
* - MM_GET_TREE_RETURN_KID_COUNT (FALSE):
* A count of the number of children each tree entry has (MM_GET_TREE_MMTID
* mode)
* - MM_GET_TREE_RETURN_MTIME (FALSE):
* The muid (user ID who made the last modification) and mtime (time) of the
* modification
* - MM_GET_TREE_RETURN_NODE_COUNT (FALSE):
* If TRUE, return a count of the number of nodes assigned to each item. If
* a string or array of strings, return a count of the number of nodes of
* that type.
* - MM_GET_TREE_RETURN_PERMS (none):
* If set, return whether or not the user can perform that action
* (MM_PERMS_READ, MM_PERMS_WRITE, MM_PERMS_SUB, MM_PERMS_APPLY,
* MM_PERMS_IS_USER, MM_PERMS_IS_GROUP, MM_PERMS_IS_RECYCLE_BIN,
* MM_PERMS_IS_RECYCLED). Only (MM_PERMS_READ, MM_PERMS_WRITE, MM_PERMS_SUB,
* MM_PERMS_APPLY) are supported when [MM_GET_TREE_NODE] is used. The
* requested permission can either be a single value or an array. If an
* empty array or TRUE is passed, all permissions are returned.
* - MM_GET_TREE_RETURN_TREE (FALSE):
* Attributes from the mm_tree table (MM_GET_TREE_MMTID mode)
* - MM_GET_TREE_SORT (FALSE):
* If TRUE, sort the entries according to sort_idx; always TRUE when
* MM_GET_TREE_DEPTH != 0
* - MM_GET_TREE_USER (current user):
* User object to test permissions against
* - MM_GET_TREE_WHERE (none):
* Add a WHERE clause to the outermost query
* If none of ([...USERS], [...GROUPS], [...NORMAL]) is TRUE, all types are
* retrieved by the query.
* @return string|void
* The query string
*/
function mm_content_get_query(array $params) {
static $user_access;
/** @var NodeInterface $params[MM_GET_TREE_NODE] */
if (!isset($user_access)) {
$user_access = &drupal_static(__FUNCTION__);
}
$defaults = [
Constants::MM_GET_TREE_BIAS_ANON => TRUE,
Constants::MM_GET_TREE_DEPTH => 0,
Constants::MM_GET_TREE_FAKE_READ_BINS => FALSE,
Constants::MM_GET_TREE_FILTER_BINS => TRUE,
Constants::MM_GET_TREE_FILTER_DOTS => TRUE,
Constants::MM_GET_TREE_FILTER_GROUPS => FALSE,
Constants::MM_GET_TREE_FILTER_NORMAL => FALSE,
Constants::MM_GET_TREE_FILTER_USERS => FALSE,
Constants::MM_GET_TREE_INNER_FILTER => '',
Constants::MM_GET_TREE_RETURN_BINS => FALSE,
Constants::MM_GET_TREE_RETURN_BLOCK => FALSE,
Constants::MM_GET_TREE_RETURN_FLAGS => FALSE,
Constants::MM_GET_TREE_RETURN_KID_COUNT => FALSE,
Constants::MM_GET_TREE_RETURN_MTIME => FALSE,
Constants::MM_GET_TREE_RETURN_TREE => FALSE,
Constants::MM_GET_TREE_USER => \Drupal::currentUser(),
Constants::MM_GET_TREE_WHERE => '',
];
$params = array_merge($defaults, $params);
if (!$params[Constants::MM_GET_TREE_FILTER_GROUPS] && !$params[Constants::MM_GET_TREE_FILTER_USERS] && !$params[Constants::MM_GET_TREE_FILTER_NORMAL]) {
$params[Constants::MM_GET_TREE_FILTER_GROUPS] = $params[Constants::MM_GET_TREE_FILTER_USERS] = $params[Constants::MM_GET_TREE_FILTER_NORMAL] = TRUE;
}
$is_node = isset($params[Constants::MM_GET_TREE_NODE]);
if (isset($params[Constants::MM_GET_TREE_RETURN_PERMS]) && $params[Constants::MM_GET_TREE_RETURN_PERMS] === TRUE ||
($is_node ? !isset($params[Constants::MM_GET_TREE_RETURN_PERMS]) || !$params[Constants::MM_GET_TREE_RETURN_PERMS] :
isset($params[Constants::MM_GET_TREE_RETURN_PERMS]) && empty($params[Constants::MM_GET_TREE_RETURN_PERMS]))) {
$params[Constants::MM_GET_TREE_RETURN_PERMS] = [Constants::MM_PERMS_WRITE, Constants::MM_PERMS_SUB, Constants::MM_PERMS_APPLY, Constants::MM_PERMS_READ, Constants::MM_PERMS_IS_USER, Constants::MM_PERMS_IS_GROUP, Constants::MM_PERMS_IS_RECYCLE_BIN, Constants::MM_PERMS_IS_RECYCLED];
}
elseif (!isset($params[Constants::MM_GET_TREE_RETURN_PERMS])) {
$params[Constants::MM_GET_TREE_RETURN_PERMS] = [];
}
elseif (!is_array($params[Constants::MM_GET_TREE_RETURN_PERMS])) {
$params[Constants::MM_GET_TREE_RETURN_PERMS] = [$params[Constants::MM_GET_TREE_RETURN_PERMS]];
}
$perms = array_flip($params[Constants::MM_GET_TREE_RETURN_PERMS]);
if (!$is_node && empty($params[Constants::MM_GET_TREE_MMTID])) {
\Drupal::logger('mm')->error('mm_content_get_query() called without a node ID or MM tree ID.', []);
return;
}
// TODO: fix for recursive where mmtid<0
if (!$is_node && $params[Constants::MM_GET_TREE_DEPTH] == 0 && $params[Constants::MM_GET_TREE_MMTID] < 0) {
// virtual user directory (A-Z)
return 'SELECT 0 AS ' . Constants::MM_PERMS_WRITE . ', 0 AS ' . Constants::MM_PERMS_SUB . ', 0 AS ' . Constants::MM_PERMS_APPLY . ', 0 AS ' . Constants::MM_PERMS_READ . ', 1 AS ' . Constants::MM_PERMS_IS_USER . ', 0 AS ' . Constants::MM_PERMS_IS_GROUP . ', 0 AS ' . Constants::MM_PERMS_IS_RECYCLE_BIN . ', 0 AS ' . Constants::MM_PERMS_IS_RECYCLED;
}
$uid = $params[Constants::MM_GET_TREE_USER]->id();
$is_admin = $uid == 1;
if (!isset($user_access[$uid])) {
foreach (['administer all menus', 'administer all users', 'administer all groups', 'view all menus'] as $access_mode) {
if ($uid || str_starts_with($access_mode, 'view')) {
$user_access[$uid][$access_mode] = $params[Constants::MM_GET_TREE_USER]->hasPermission($access_mode);
}
else {
$user_access[$uid][$access_mode] = FALSE;
}
}
}
$is_admin |= $user_access[$uid]['administer all menus'];
$outside_selects = [];
if (isset($params[Constants::MM_GET_TREE_ADD_SELECT])) {
if (is_array($params[Constants::MM_GET_TREE_ADD_SELECT])) {
$outside_selects = $params[Constants::MM_GET_TREE_ADD_SELECT];
}
else {
$outside_selects[] = $params[Constants::MM_GET_TREE_ADD_SELECT];
}
}
if (isset($params[Constants::MM_GET_TREE_RETURN_NODE_COUNT]) && !empty($params[Constants::MM_GET_TREE_RETURN_NODE_COUNT])) {
if (is_array($params[Constants::MM_GET_TREE_RETURN_NODE_COUNT])) {
$compare = " AND node.type IN ('" . join("', '", $params[Constants::MM_GET_TREE_RETURN_NODE_COUNT]) . "')";
}
elseif (is_string($params[Constants::MM_GET_TREE_RETURN_NODE_COUNT])) {
$compare = " AND node.type = '" . $params[Constants::MM_GET_TREE_RETURN_NODE_COUNT] . "'";
}
else {
$compare = '';
}
$outside_selects[] = "(SELECT COUNT(DISTINCT n.nid) FROM {mm_node2tree} n INNER JOIN {node} node ON node.nid = n.nid WHERE n.mmtid = o.container$compare) AS nodecount";
}
$outside_group_by = $node_selects = [];
$outside_where = $params[Constants::MM_GET_TREE_WHERE];
$inside_joins = $outside_joins = $outside_order_by = $anon_group = '';
$inside_selects = $inside_group_by = ['i.mmtid'];
$having = '';
$filter_dots = "(SUBSTR(name, 1, 1) <> '.' OR name IN('" . Constants::MM_ENTRY_NAME_GROUPS . "', '" . Constants::MM_ENTRY_NAME_USERS . "', '" . Constants::MM_ENTRY_NAME_VIRTUAL_GROUP . "'))";
$inside_selects[] = 'i.container';
$inside_group_by[] = 'i.container, i.name';
$outside_group_by[] = 'o.container';
if (!$is_node && (!$params[Constants::MM_GET_TREE_FILTER_GROUPS] || !$params[Constants::MM_GET_TREE_FILTER_USERS] || !$params[Constants::MM_GET_TREE_FILTER_NORMAL] || !$params[Constants::MM_GET_TREE_FILTER_BINS] || !$params[Constants::MM_GET_TREE_FILTER_DOTS])) {
$havings = [];
if ($params[Constants::MM_GET_TREE_FILTER_GROUPS]) {
$perms[Constants::MM_PERMS_IS_GROUP] = 1;
$havings[] = 'SUM(o.is_group) > 0';
}
if ($params[Constants::MM_GET_TREE_FILTER_NORMAL]) {
$perms[Constants::MM_PERMS_IS_USER] = $perms[Constants::MM_PERMS_IS_GROUP] = 1;
if ($params[Constants::MM_GET_TREE_FILTER_USERS] && !$params[Constants::MM_GET_TREE_FILTER_GROUPS]) {
$havings[] = 'SUM(o.is_group) = 0';
}
else {
$havings[] = 'SUM(o.is_user) = 0 AND SUM(o.is_group) = 0';
}
}
elseif ($params[Constants::MM_GET_TREE_FILTER_USERS]) {
$perms[Constants::MM_PERMS_IS_USER] = 1;
$havings[] = 'SUM(o.is_user) > 0';
}
$having = join(' OR ', $havings);
$condit = [];
if (!$params[Constants::MM_GET_TREE_FILTER_DOTS]) {
$condit[] = $filter_dots;
$outside_group_by[] = 'name'; // Needed for HAVING clause
}
if (!$params[Constants::MM_GET_TREE_FILTER_BINS]) {
$condit[] = 'SUM(o.is_recycled) = 0';
}
if (!$condit) {
$having = ' HAVING ' . $having;
}
elseif ($having) {
$having = ' HAVING (' . $having . ') AND ' . join(' AND ', $condit);
}
else {
$having = ' HAVING ' . join(' AND ', $condit);
}
if (!$outside_group_by) {
$outside_group_by[] = 'o.mmtid';
}
}
if ($perms) {
if ($uid) {
if (!$is_admin) {
$inside_joins .=
'LEFT JOIN {mm_tree_access} a ON a.mmtid = i.mmtid ' .
'LEFT JOIN {mm_group} g ON g.gid = a.gid ' .
"LEFT JOIN {mm_virtual_group} v ON v.vgid = g.vgid AND v.uid = $uid ";
}
}
elseif ($params[Constants::MM_GET_TREE_BIAS_ANON]) {
$anon_group = 'SUM(o.is_group) = 0 AND ';
}
if ($is_admin) {
foreach ([Constants::MM_PERMS_WRITE, Constants::MM_PERMS_SUB, Constants::MM_PERMS_APPLY, Constants::MM_PERMS_READ] as $m) {
if (isset($perms[$m])) {
$outside_selects[$m] = "COUNT(*) > 0 AS $m";
}
}
$outside_selects[] = '1 AS ' . Constants::MM_PERMS_ADMIN;
}
else {
$outside_admin = [];
if (!empty($user_access[$uid]['administer all groups'])) {
$outside_admin[] = 'SUM(o.is_group) > 0';
}
if (!empty($user_access[$uid]['administer all users'])) {
$outside_admin[] = 'SUM(o.is_user) > 0';
}
$outside_admin = count($outside_admin) ? join(' OR ', $outside_admin) . ' OR ' : $anon_group;
foreach ([Constants::MM_PERMS_WRITE, Constants::MM_PERMS_SUB, Constants::MM_PERMS_APPLY] as $m) {
if (isset($perms[$m])) {
if ($uid) {
$mode_cmp = $m == Constants::MM_PERMS_WRITE ? "= '" . Constants::MM_PERMS_WRITE . "'" : "IN ('" . Constants::MM_PERMS_WRITE . "', '$m')";
$not_anon = "a.mode $mode_cmp AND (v.uid = $uid OR g.vgid = 0 AND g.uid = $uid) OR i.uid = $uid OR ";
}
else $not_anon = ''; // ignore anon user when owner or in a group
$outside_selects[$m] = "{$outside_admin}(SUM(o.container = o.mmtid AND o.can_$m) > 0" . ($user_access[$uid]['view all menus'] ? '' : ' AND COUNT(*) = SUM(o.can_r)') . ") AS $m";
$mode_cmp = "LOCATE('" . Constants::MM_PERMS_WRITE . "', i.default_mode) > 0";
if ($m != Constants::MM_PERMS_WRITE) $mode_cmp .= " OR LOCATE('$m', i.default_mode) > 0";
$inside_selects[$m] = "SUM($not_anon$mode_cmp) > 0 AS can_$m";
$node_selects[] = "SUM($m) > 0 AS $m";
}
}
if (isset($perms[Constants::MM_PERMS_READ])) {
if ($user_access[$uid]['view all menus']) {
$outside_selects[] = 'SUM(o.is_recycled) = 0 AND (SUM(o.is_group) = 0 AND COUNT(*) > 0 OR COUNT(*) = SUM(o.can_r)) AS r';
}
elseif ($is_node) {
$outside_selects[] = "{$outside_admin}COUNT(o.container = o.mmtid) = SUM(o.can_r) AS r";
}
else {
$outside_selects[] = "{$outside_admin}IF(SUM(o.is_group), SUM(o.container = o.mmtid AND o.can_m) > 0, COUNT(o.container = o.mmtid) = SUM(o.can_r)) AS r";
}
$node_selects[] = 'SUM(r) > 0 AS r';
}
$not_anon = $uid ? "(v.uid = $uid OR g.vgid = 0 AND g.uid = $uid) OR i.uid = $uid OR " : '';
if ($params[Constants::MM_GET_TREE_FAKE_READ_BINS]) {
$not_anon .= "i.name = '" . Constants::MM_ENTRY_NAME_RECYCLE . "' OR ";
}
$inside_selects[] = "SUM({$not_anon}i.default_mode <> '') > 0 AS can_r";
if (!$is_node) {
$not_anon = $uid ? "a.mode IN ('" . Constants::MM_PERMS_WRITE . "', '" . Constants::MM_PERMS_SUB . "', '" . Constants::MM_PERMS_READ . "') AND (v.uid = $uid OR g.vgid = 0 AND g.uid = $uid) OR i.uid = $uid OR " : '';
$inside_selects[] = "SUM({$not_anon}LOCATE('" . Constants::MM_PERMS_WRITE . "', i.default_mode) > 0 OR LOCATE('" . Constants::MM_PERMS_SUB . "', i.default_mode) > 0 OR LOCATE('" . Constants::MM_PERMS_READ . "', i.default_mode) > 0) > 0 AS can_m";
}
}
if (isset($perms[Constants::MM_PERMS_IS_USER]) || $user_access[$uid]['administer all users']) {
$inside_selects[] = 'i.mmtid = ' . mm_content_users_mmtid() . ' AS is_user';
if (isset($perms[Constants::MM_PERMS_IS_USER]) && !$is_node) {
$outside_selects[] = 'SUM(o.is_user) > 0 AS ' . Constants::MM_PERMS_IS_USER;
}
}
if (isset($perms[Constants::MM_PERMS_IS_GROUP]) || !empty($anon_group) || $user_access[$uid]['administer all groups'] || $user_access[$uid]['view all menus']) {
$inside_selects[] = 'i.mmtid = ' . mm_content_groups_mmtid() . ' AS is_group';
if (isset($perms[Constants::MM_PERMS_IS_GROUP]) && !$is_node) {
$outside_selects[] = empty($anon_group) ? 'SUM(o.is_group) > 0 AS ' . Constants::MM_PERMS_IS_GROUP : '0 AS ' . Constants::MM_PERMS_IS_GROUP;
}
}
if (isset($perms[Constants::MM_PERMS_IS_RECYCLE_BIN])) {
$inside_selects[] = "i.mmtid = i.container AND i.name = '" . Constants::MM_ENTRY_NAME_RECYCLE . "' AS is_recycle_bin";
$outside_selects[] = 'SUM(o.container = o.mmtid AND o.is_recycle_bin) > 0 AS ' . Constants::MM_PERMS_IS_RECYCLE_BIN;
$node_selects[] = 'SUM(' . Constants::MM_PERMS_IS_RECYCLE_BIN . ') AS ' . Constants::MM_PERMS_IS_RECYCLE_BIN;
}
if (isset($perms[Constants::MM_PERMS_IS_RECYCLED]) || $params[Constants::MM_GET_TREE_RETURN_BINS] || $user_access[$uid]['view all menus'] || !$params[Constants::MM_GET_TREE_FILTER_BINS]) {
$inside_selects[] = "SUM(i.name = '" . Constants::MM_ENTRY_NAME_RECYCLE . "') AS is_recycled";
if (isset($perms[Constants::MM_PERMS_IS_RECYCLED]) || !$params[Constants::MM_GET_TREE_FILTER_BINS]) {
$outside_selects[] = 'SUM(o.is_recycled) > 0 AS ' . Constants::MM_PERMS_IS_RECYCLED;
$node_selects[] = 'SUM(' . Constants::MM_PERMS_IS_RECYCLED . ') AS ' . Constants::MM_PERMS_IS_RECYCLED;
}
if ($params[Constants::MM_GET_TREE_RETURN_BINS]) {
$inside_selects[] = "IF(i.name = '" . Constants::MM_ENTRY_NAME_RECYCLE . "', i.mmtid, NULL) AS recycle_bins";
$outside_selects[] = 'GROUP_CONCAT(o.recycle_bins ORDER BY o.recycle_bins) AS recycle_bins';
$node_selects[] = 'GROUP_CONCAT(recycle_bins) AS recycle_bins';
}
}
} // if ($perms)
if ($is_node) {
$i =
'SELECT t.mmtid, t.default_mode, t.uid, t.name, n2.mmtid AS container ' .
'FROM {mm_tree_parents} p ' .
'LEFT JOIN {mm_tree} t ON t.mmtid = p.parent ' .
'INNER JOIN {mm_node2tree} n2 ON n2.mmtid = p.mmtid ' .
'WHERE n2.nid = ' . $params[Constants::MM_GET_TREE_NODE]->id() . ' ' .
'UNION SELECT t.mmtid, t.default_mode, t.uid, t.name, n2.mmtid AS container ' .
'FROM {mm_tree} t ' .
'LEFT JOIN {mm_node2tree} n2 ON n2.mmtid = t.mmtid ' .
'WHERE n2.nid = ' . $params[Constants::MM_GET_TREE_NODE]->id();
}
else {
if ($params[Constants::MM_GET_TREE_RETURN_TREE] || $params[Constants::MM_GET_TREE_DEPTH] || $params[Constants::MM_GET_TREE_RETURN_MTIME]) {
if ($params[Constants::MM_GET_TREE_RETURN_TREE]) {
$outside_selects[] = mm_all_db_columns('mm_tree', [], 't.');
}
if ($params[Constants::MM_GET_TREE_DEPTH] && $params[Constants::MM_GET_TREE_SORT]) {
$outside_order_by = ' ORDER BY sort_idx';
}
if ($perms || $params[Constants::MM_GET_TREE_DEPTH] || $params[Constants::MM_GET_TREE_RETURN_MTIME]) {
$outside_joins .= ' INNER JOIN {mm_tree} t ON t.mmtid = o.container';
}
}
if ($params[Constants::MM_GET_TREE_RETURN_FLAGS]) {
$outside_selects[] = "(SELECT GROUP_CONCAT(CONCAT_WS('|1', flag, data) SEPARATOR '|2') FROM {mm_tree_flags} WHERE mmtid = o.container) AS flags";
}
if ($params[Constants::MM_GET_TREE_RETURN_MTIME]) {
$outside_selects[] = 'MAX(tr.muid) AS muid, MAX(tr.mtime) AS mtime';
$outside_joins .= ' LEFT JOIN {mm_tree_revision} tr ON tr.vid = t.vid';
}
if ($params[Constants::MM_GET_TREE_RETURN_BLOCK]) {
$outside_selects[] = 'MAX(b.bid) AS bid, MAX(b.max_depth) AS max_depth, MAX(b.max_parents) AS max_parents';
$outside_joins .= ' LEFT JOIN {mm_tree_block} b ON b.mmtid = o.container';
}
if ($params[Constants::MM_GET_TREE_RETURN_KID_COUNT]) {
$condit = [];
if (!$params[Constants::MM_GET_TREE_FILTER_BINS]) {
$condit[] = "name <> '" . Constants::MM_ENTRY_NAME_RECYCLE . "'";
}
if (!$params[Constants::MM_GET_TREE_FILTER_HIDDEN]) {
$condit[] = "NOT hidden";
}
if (!$params[Constants::MM_GET_TREE_FILTER_DOTS]) {
$condit[] = $filter_dots;
}
if ($condit) {
$outside_selects[] = '(SELECT COUNT(*) FROM {mm_tree} WHERE parent = o.container AND ' . join(' AND ', $condit) . ') AS kids';
}
else {
$outside_selects[] = '(SELECT COUNT(*) FROM {mm_tree} WHERE parent = o.container) AS kids';
}
}
switch ($params[Constants::MM_GET_TREE_DEPTH]) {
case -1:
$i =
// the item
'SELECT mmtid AS container, mmtid, default_mode, uid, name ' .
'FROM {mm_tree} ' .
'WHERE mmtid = ' . $params[Constants::MM_GET_TREE_MMTID] . ' ' .
// the item's children
'UNION SELECT p.mmtid, t.mmtid, t.default_mode, t.uid, t.name ' .
'FROM {mm_tree_parents} p ' .
'INNER JOIN {mm_tree} t ON t.mmtid = p.mmtid ' .
'WHERE p.parent = ' . $params[Constants::MM_GET_TREE_MMTID];
if ($perms) {
$i .= ' ' .
// the item's parents
'UNION SELECT p.mmtid, t.mmtid, t.default_mode, t.uid, t.name ' .
'FROM {mm_tree_parents} p ' .
'INNER JOIN {mm_tree} t ON t.mmtid = p.parent ' .
'WHERE p.mmtid = ' . $params[Constants::MM_GET_TREE_MMTID] . ' ' .
// its children's parents
'UNION SELECT p0.mmtid, t.mmtid, t.default_mode, t.uid, t.name ' .
'FROM {mm_tree_parents} p0 ' .
'INNER JOIN {mm_tree_parents} p1 ON p1.mmtid = p0.mmtid ' .
'INNER JOIN {mm_tree} t ON t.mmtid = p1.parent ' .
'WHERE p0.parent = ' . $params[Constants::MM_GET_TREE_MMTID];
}
break;
case 0:
if (!$perms) {
return
'SELECT ' . join(', ', $outside_selects) . ' FROM {mm_tree} t' .
$outside_joins .
(empty($params[Constants::MM_GET_TREE_WHERE]) ? '' : ' WHERE ' . $params[Constants::MM_GET_TREE_WHERE]);
}
$i =
// the item
'SELECT mmtid AS container, mmtid, default_mode, uid, name ' .
'FROM {mm_tree} ' .
'WHERE mmtid = ' . $params[Constants::MM_GET_TREE_MMTID] . ' ' .
// its parents
'UNION SELECT p.mmtid, t.mmtid, t.default_mode, t.uid, t.name ' .
'FROM {mm_tree_parents} p ' .
'INNER JOIN {mm_tree} t ON t.mmtid = p.parent ' .
'WHERE p.mmtid = ' . $params[Constants::MM_GET_TREE_MMTID];
break;
case 1:
$i =
// the item
'SELECT t.mmtid AS container, t.mmtid, t.default_mode, t.uid, t.name ' .
'FROM {mm_tree} t ' .
'WHERE t.mmtid = ' . $params[Constants::MM_GET_TREE_MMTID] . ' ' .
// the item's immediate children
'UNION SELECT t.mmtid AS container, t.mmtid, t.default_mode, t.uid, t.name ' .
'FROM {mm_tree} t ' .
'WHERE t.parent = ' . $params[Constants::MM_GET_TREE_MMTID] . $params[Constants::MM_GET_TREE_INNER_FILTER];
if ($perms) {
$i .= ' ' .
// the item's parents
'UNION SELECT p.mmtid, t.mmtid, t.default_mode, t.uid, t.name ' .
'FROM {mm_tree_parents} p ' .
'INNER JOIN {mm_tree} t ON t.mmtid = p.parent ' .
'WHERE p.mmtid = ' . $params[Constants::MM_GET_TREE_MMTID] . ' ' .
// its immediate children's parents
'UNION SELECT t.mmtid, t2.mmtid, t2.default_mode, t2.uid, t2.name ' .
'FROM {mm_tree} t ' .
'INNER JOIN {mm_tree_parents} p ON t.mmtid = p.mmtid ' .
'INNER JOIN {mm_tree} t2 ON t2.mmtid = p.parent ' .
'WHERE t.parent = ' . $params[Constants::MM_GET_TREE_MMTID] . $params[Constants::MM_GET_TREE_INNER_FILTER];
}
break;
default:
$select = Database::getConnection()->select('mm_tree_parents', 'p');
$select->addExpression('MAX(p.depth)', 'depth');
$select->condition('p.mmtid', $params[Constants::MM_GET_TREE_MMTID]);
$depth = (int) $select->execute()->fetchField() + (int) $params[Constants::MM_GET_TREE_DEPTH] + 1;
$i =
'SELECT x.container, t.mmtid, t.default_mode, t.uid, t.name ' .
'FROM (' .
'SELECT p0.mmtid AS container, p1.parent AS mmtid ' .
'FROM (' .
'SELECT p0.mmtid ' .
'FROM {mm_tree_parents} p0 ' .
'INNER JOIN {mm_tree_parents} p1 ON p1.mmtid = p0.mmtid ' .
'WHERE p0.parent = ' . $params[Constants::MM_GET_TREE_MMTID] . ' ' .
'GROUP BY p0.mmtid HAVING MAX(p1.depth) < ' . $depth .
') p0 ' .
'INNER JOIN {mm_tree_parents} p1 ON p1.mmtid = p0.mmtid ' .
'UNION (' .
'SELECT p0.mmtid, p0.mmtid ' .
'FROM {mm_tree_parents} p0 ' .
'INNER JOIN {mm_tree_parents} p1 ON p1.mmtid = p0.mmtid ' .
'WHERE p0.parent = ' . $params[Constants::MM_GET_TREE_MMTID] . ' ' .
'GROUP BY p0.mmtid HAVING MAX(p1.depth) < ' . $depth .
') ' .
'UNION SELECT ' . $params[Constants::MM_GET_TREE_MMTID] . ', parent ' .
'FROM {mm_tree_parents} ' .
'WHERE mmtid = ' . $params[Constants::MM_GET_TREE_MMTID] . ' ' .
'UNION SELECT ' . $params[Constants::MM_GET_TREE_MMTID] . ', ' . $params[Constants::MM_GET_TREE_MMTID] .
') AS x ' .
'INNER JOIN {mm_tree} t ON t.mmtid = x.mmtid';
break;
}
}
// Note: Always use ORDER BY NULL instead of GROUP BY without ORDER BY, as
// this is slightly faster.
$query =
'SELECT ' . join(', ', $outside_selects) . ' FROM (' .
'SELECT ' . join(', ', $inside_selects) . ' ' .
'FROM (' .
$i .
') AS i ' .
$inside_joins .
'GROUP BY ' . join(', ', $inside_group_by) . ' ' .
'ORDER BY NULL' .
') AS o' .
$outside_joins .
(empty($outside_where) ? '' : ' WHERE ' . $outside_where) .
($outside_group_by ? ' GROUP BY ' . join(', ', $outside_group_by) . $having : '') .
($outside_order_by ?: ($outside_group_by ? ' ORDER BY NULL' : ''));
if ($is_node && $node_selects && !$is_admin) {
return 'SELECT ' . join(', ', $node_selects) . " FROM ($query) q";
}
return $query;
}
/**
* Clear one element of the caches used by various functions, or completely
* clear all caches.
*
* @param int|null $mmtid
* If set, delete just the cached mm_content_user_can data for this tree ID or
* array of tree IDs. Otherwise, clear the whole cache.
*/
function mm_content_clear_caches($mmtid = NULL) {
$_mmtbt_cache = &drupal_static('_mmtbt_cache', []);
$_mmgp_cache = &drupal_static('_mmgp_cache', []);
$_mmuc_cache = &drupal_static('_mmuc_cache', []);
$_mmucr_cache = &drupal_static('_mmucr_cache', []);
$_mmcucn_cache = &drupal_static('_mmcucn_cache');
$_mm_custom_url_rewrite_outbound_cache = &drupal_static('_mm_custom_url_rewrite_outbound_cache');
if (isset($mmtid)) {
if (is_array($mmtid)) {
$flipped = array_flip($mmtid);
$_mmuc_cache = array_diff_key($_mmuc_cache, $flipped);
$_mmucr_cache = array_diff_key($_mmucr_cache, $flipped);
$_mmtbt_cache = array_diff_key($_mmtbt_cache, $flipped);
$_mmgp_cache = array_diff_key($_mmgp_cache, $flipped);
}
else {
unset($_mmuc_cache[$mmtid]);
unset($_mmucr_cache[$mmtid]);
unset($_mmtbt_cache[$mmtid]);
unset($_mmgp_cache[$mmtid]);
}
_mm_content_clear_access_cache($mmtid);
}
else {
$_mmuc_cache = $_mmucr_cache = $_mmtbt_cache = $_mmgp_cache = [];
}
// Always clear arrays not indexed by mmtid
$_mmcucn_cache = $_mm_custom_url_rewrite_outbound_cache = [];
}
/**
* Queue mm_tree entries that have changed, or update the sort index for all
* queued entries.
*
* If neither $parent nor $child is set, perform all queued updates. This
* usually happens during monster_menus_exit().
*
* @param int|null $parent
* If set, queue this portion of the tree for future update; this should be
* the parent term ID of the entry that has changed. If the parent is not
* known, use NULL and specify a value in $child, instead.
* @param int|null $child
* Instead of using $parent, a $child can be specified. In this case, the
* parent is queried in an additional step, so this method should be avoided
* when possible.
* @param bool $all
* If TRUE, update all entries, not just the dirty ones; this is generally
* only done the very first time the sort index is generated.
* @param int $semaphore_time
* If non-zero, use a semaphore to ensure exclusive access to updates
* performed when neither $parent nor $child is set. If another process has
* set the semaphore, the current process will wait for up to this number of
* seconds for it to clear. This number must be less than 2 hours. Do not use
* this parameter during normal page updates (such as hook_exit()), as they
* should never block.
*/
function mm_content_update_sort_queue($parent = NULL, $child = NULL, $all = FALSE, $semaphore_time = 0) {
static $drupal_static_fast;
$_mm_content_defer_sort_index_update = &drupal_static('_mm_content_defer_sort_index_update');
if (!isset($drupal_static_fast)) {
// This is cumbersome, but assigning to an array is the only way that works.
$drupal_static_fast['q'] = &drupal_static(__FUNCTION__, []);
}
$queue = &$drupal_static_fast['q'];
if (empty($parent) && !empty($child)) {
$parent = mm_content_get_parent($child);
if (empty($parent)) {
return;
}
}
if (!empty($parent)) {
if (isset($queue[$parent])) {
$queue[$parent] |= $all;
}
else {
$queue[$parent] = $all;
}
}
elseif ($queue) {
if (!empty($_mm_content_defer_sort_index_update)) {
return;
}
$uniq = array_unique($queue);
if (count($uniq) == 1 && !$uniq[mm_ui_mmlist_key0($uniq)]) {
mm_content_update_sort(array_keys($queue), FALSE, $semaphore_time);
}
else {
foreach ($queue as $parent => $all) {
mm_content_update_sort($parent, $all, $semaphore_time);
}
}
$queue = [];
}
}
/**
* Update the mm_tree column containing the sort index.
*
* @param int $mmtid
* Point from which to update entries downward in the tree. Can be an array of
* values when $all is FALSE.
* @param bool $all
* If TRUE, update all entries, not just the dirty ones; this is generally
* only done the very first time the sort index is generated.
* @param int $semaphore_time
* If non-zero, use a semaphore to ensure exclusive access to updates. If
* another process has set the semaphore, the current process will wait for
* up to this number of seconds for it to clear. If it does not clear in time,
* a textual error message is returned. This number must be less than 2 hours.
* Do not use this parameter during normal page updates (such as hook_exit()),
* as they should never block.
* @return TranslatableMarkup|void
* If $semaphore_time is non-zero and the semaphore could not be grabbed, an
* error message is returned.
*/
function mm_content_update_sort($mmtid = 1, $all = TRUE, $semaphore_time = 0) {
$database = Database::getConnection();
if ($semaphore_time) {
$sem_err = mm_content_update_sort_test_semaphore($semaphore_time);
if ($sem_err instanceof TranslatableMarkup) {
return $sem_err;
}
}
if ($all) {
$update_all = function ($sort_idx, $mmtid) use (&$update_all, $database) {
$order = 0;
$select = $database->select('mm_tree', 't');
$select->addField('t', 'mmtid');
$select->addExpression('IF(hidden, 1, IF(name = :name, 2, 0))', 'sort_column', [':name' => Constants::MM_ENTRY_NAME_RECYCLE]);
$select->condition('t.parent', $mmtid);
$select->orderBy('sort_column')
->orderBy('weight')
->orderBy('name');
$result = $select->execute();
foreach ($result as $r) {
$new_idx = $sort_idx . _mm_content_btoa($order++);
_mm_content_test_sort_length($new_idx, $r->mmtid, TRUE);
$database->update('mm_tree')
->fields(['sort_idx' => $new_idx, 'sort_idx_dirty' => 0])
->condition('mmtid', $r->mmtid)
->execute();
$update_all($new_idx, $r->mmtid);
}
};
if ($mmtid <= 1) {
$mmtid = 1;
$sort_idx = '';
$database->update('mm_tree')
->fields(['sort_idx' => $sort_idx, 'sort_idx_dirty' => 0])
->condition('mmtid', $mmtid)
->execute();
}
else {
$sort_idx = $database->select('mm_tree', 't')
->fields('t', ['sort_idx'])
->condition('t.mmtid', $mmtid)
->execute()->fetchField();
}
$update_all($sort_idx, $mmtid);
}
else {
// Start a transaction.
$txn = $database->startTransaction();
for ($last = -1;;) {
$in = is_array($mmtid) ? 'IN (:mmtid[])' : '= :mmtid';
$parent_q = $database->queryRange(
'SELECT t.parent, t.sort_idx, t.mmtid ' .
'FROM {mm_tree} t ' .
'INNER JOIN {mm_tree_parents} p ON p.mmtid = t.mmtid ' .
"WHERE t.sort_idx_dirty = 1 AND p.parent $in " .
'ORDER BY LENGTH(t.sort_idx) DESC',
0, 1, [(is_array($mmtid) ? ':mmtid[]' : ':mmtid') => $mmtid]
);
if ($parent = $parent_q->fetchObject()) {
// Prevent the outer for loop from running amok if there is a DB error
if ($parent->mmtid == $last) {
break;
}
$last = $parent->mmtid;
$sort_idx = substr_replace($parent->sort_idx, '', -Constants::MM_CONTENT_BTOA_CHARS);
$sort_len = strlen($sort_idx);
$order = 0;
$quick = [];
$select = $database->select('mm_tree', 't');
$select->fields('t', ['mmtid', 'sort_idx', 'sort_idx_dirty']);
$select->addExpression(
'IF(t.hidden, :hidden_weight, IF(name = :recycle_name, :recycle_weight, :normal_weight))',
'sort_group',
[
':hidden_weight' => 1,
':recycle_name' => Constants::MM_ENTRY_NAME_RECYCLE,
':recycle_weight' => 2,
':normal_weight' => 0,
]
);
$select->condition('t.parent', $parent->parent);
$select->orderBy('sort_group')
->orderBy('t.weight')
->orderBy('t.name');
$result = $select->execute();
foreach ($result as $r) {
$new_idx = _mm_content_btoa($order++);
if ($new_idx != substr($r->sort_idx, -Constants::MM_CONTENT_BTOA_CHARS)) {
// Make sure $r->mmtid is passed to the query as an integer, as that
// is much faster.
mm_content_execute_typed_query($database,
'UPDATE {mm_tree} t ' .
'INNER JOIN {mm_tree_parents} p ON p.mmtid = t.mmtid ' .
'SET t.sort_idx = CONCAT(' .
'SUBSTRING(t.sort_idx, :sort_start, :sort_len), :new_idx, SUBSTRING(t.sort_idx, :my_idx)' .
'), t.sort_idx_dirty = :dirty ' .
'WHERE p.parent = :mmtid OR t.mmtid = :mmtid',
[
':sort_start' => 1,
':sort_len' => $sort_len,
':new_idx' => $new_idx,
':my_idx' => $sort_len + Constants::MM_CONTENT_BTOA_CHARS + 1,
':dirty' => 0,
':mmtid' => (int) $r->mmtid,
]
);
}
elseif ($r->sort_idx_dirty) {
$quick[] = $r->mmtid;
}
}
if ($quick) {
$database->update('mm_tree')
->fields(['sort_idx_dirty' => 0])
->condition('mmtid', $quick, 'IN')
->execute();
}
}
else {
break;
}
}
}
unset($txn);
if ($semaphore_time) {
mm_content_update_sort_test_semaphore(-1);
}
}
/**
* By default, Drupal binds all SQL query arguments using strings. For some
* queries, it is much more efficient if, for instance, integers are passed as
* integers.
*
* @param Connection $db
* The database connection.
* @param string $statement
* The query to execute.
* @param mixed[] $args
* An array of arguments, each of which must be either a string or an integer.
* This type is used when binding it to the query.
*
* @return StatementInterface
*/
function mm_content_execute_typed_query(Connection $db, $statement, $args) {
if ($db->databaseType() !== 'mysql') {
return $db->query($statement, $args);
}
$ps = $db->prepareStatement($statement, $db->getConnectionOptions());
$sth = $ps->getClientStatement();
foreach ($args as $name => $value) {
if (is_integer($value)) {
$type = \PDO::PARAM_INT;
}
else if (is_string($value)) {
$type = \PDO::PARAM_STR;
}
else {
throw new InvalidArgumentException('Only single integer and string parameters are supported.');
}
$sth->bindValue($name, $value, $type);
}
$sth->execute();
return $ps;
}
/**
* Use a semaphore to ensure exclusive access to sort index updates.
*
* @param int $semaphore_time
* If another process has set the semaphore, the current process will wait for
* up to this number of seconds for it to clear. If it does not clear in time,
* a textual error message is returned. This number must be less than 2 hours.
*
* Pass a negative number to clear a semaphore that is owned by the current
* process.
*
* Do not use this function during normal page updates (such as hook_exit()),
* as they should never block.
* @return TranslatableMarkup|true
* Either TRUE if the semaphore was obtained, or an error message.
*/
function mm_content_update_sort_test_semaphore($semaphore_time) {
if ($semaphore_time < 0) {
\Drupal::state()->delete('mm_update_sort_semaphore');
}
else if ($semaphore_time) {
for (;;) {
// Intentionally don't use REQUEST_TIME here, because we need to get the
// true time.
$now = time();
if (!($running = \Drupal::state()->get('mm_update_sort_semaphore', 0))) {
break;
}
if ($now - $running > 2 * 60 * 60 + 5) {
\Drupal::logger('mm')->error('mm_content_update_sort() has been running for more than two hours, or terminated unexpectedly. Ignoring semaphore.');
break;
}
if ($now - $running >= $semaphore_time) {
return t('Another process already has the mm_content_update_sort() semaphore.');
}
// Wait .1 sec before polling again.
usleep(100000);
}
\Drupal::state()->set('mm_update_sort_semaphore', $now);
}
return TRUE;
}
/**
* Decode a 32-bit word represented by four ASCII characters, derived from the
* number's base-64 equivalent. This is the inverse of _mm_content_btoa().
*
* @param string $str
* The string containing the encoded integer
* @return int
* The resulting integer
*/
function _mm_content_atob($str) {
$out = 0;
while (!empty($str) || $str === '0') {
$out = $out * Constants::MM_CONTENT_BTOA_BASE + ord($str[0]) - Constants::MM_CONTENT_BTOA_START;
$str = substr($str, 1);
}
return $out;
}
/**
* Encode a 32-bit word using four ASCII characters, derived from the number's
* base-64 equivalent. This produces a sequence that is suitable for sorting in
* SQL, without sacrificing too much space. A larger base cannot be used without
* the possibility of causing case-insensitive sorting errors.
*
* @param int $uword
* The unsigned word to encode
* @return string
* The word, encoded in a string
*/
function _mm_content_btoa($uword) {
static $pows, $max;
if (!isset($pows)) {
$max = (int) Constants::MM_CONTENT_BTOA_BASE ** Constants::MM_CONTENT_BTOA_CHARS;
$pows = [];
for ($i = 1; $i < Constants::MM_CONTENT_BTOA_CHARS; $i++) {
array_unshift($pows, (int) Constants::MM_CONTENT_BTOA_BASE ** $i);
}
}
if ($uword < 0 || $uword >= $max) {
die('Range error');
}
$out = '';
foreach ($pows as $pow) {
$out .= chr(Constants::MM_CONTENT_BTOA_START + (int) floor($uword / $pow));
// The % operator doesn't work correctly, here
$uword = $uword - ((int) floor($uword / $pow) * $pow);
}
$out .= chr(Constants::MM_CONTENT_BTOA_START + $uword);
return $out;
}
/**
* Automatically empty all recycle bins of content that has been there for more
* than a set time. Run as uid=1 during monster_menus_cron().
*
* @param int $limit
* The maximum number of nodes/pages to delete
*/
function mm_content_empty_all_bins($limit = 0) {
if (($empty = mm_get_setting('recycle_auto_empty')) > 0) {
$db = Database::getConnection();
$now = mm_request_time();
$query = $db->select('mm_recycle', 'rc')
->fields('rc')
->fields('n', ['type']);
$query->leftJoin('node', 'n', "rc.type = 'node' AND n.nid = rc.id");
$query->condition('rc.recycle_date', $now - $empty, '<');
if ($limit > 0) {
$query->range(0, $limit);
}
$result = $query->execute();
$bins = $nodes = [];
foreach ($result as $r) {
if ($r->type == 'node') {
if (empty($nodes[$r->id])) {
$only_in_emptyable_bin = $db->query('SELECT COUNT(*) = 0 FROM {mm_node2tree} n2 LEFT JOIN {mm_recycle} r ON n2.nid = r.id AND `type` = :type AND (r.from_mmtid = n2.mmtid OR r.bin_mmtid = n2.mmtid) WHERE n2.nid = :nid AND (r.id IS NULL OR r.recycle_date >= :date)',
[':type' => 'node', ':nid' => $r->id, ':date' => $now - $empty])->fetchField();
if ($only_in_emptyable_bin) {
\Drupal::logger('mm')->notice('Automatically emptying nid=@nid from recycle bin', ['@nid' => $r->id]);
$r->id->delete();
// It's remotely possible for a node to be left over in mm_recycle,
// even though it has already been deleted. In this case MM's deletion
// code isn't called during node_delete(), so call it (again) now.
/** @var NodeInterface $node */
$node = Node::create(['nid' => $r->id, 'type' => $r->type]);
monster_menus_node_delete($node);
$nodes[$r->id] = TRUE;
}
else {
// Node cannot yet be deleted yet, so just remove it from this bin.
$db->delete('mm_recycle')
->condition('type', 'node')
->condition('id', $r->id)
->condition('bin_mmtid', $r->bin_mmtid)
->execute();
$db->delete('mm_node2tree')
->condition('nid', $r->id)
->condition('mmtid', $r->bin_mmtid)
->execute();
}
}
}
elseif ($r->type == 'cat') {
\Drupal::logger('mm')->notice('Automatically deleting mmtid=@mmtid from recycle bin', ['@mmtid' => $r->id]);
if (mm_content_delete($r->id, TRUE, FALSE, 10 * 60)) {
return;
}
}
$bins[$r->bin_mmtid] = 1;
}
foreach (array_keys($bins) as $bin) {
mm_content_delete_bin($bin);
}
}
}
/**
* Figure out if a given user can access a particular node. When a node belongs
* to more than one entry, logically OR the permissions for all entries.
*
* @param NodeInterface|int $node
* The node object or node number being queried
* @param string $mode
* If set, return whether the user can perform that action (MM_PERMS_READ,
* MM_PERMS_WRITE, MM_PERMS_SUB, MM_PERMS_APPLY). Otherwise, return an array
* containing each of these elements with either TRUE or FALSE values.
* @param AccountInterface $account
* User object of user to test against. Defaults to the current user.
* @return bool|array
* See above
*/
function mm_content_user_can_node($node, $mode = '', AccountInterface $account = NULL) {
static $recursive = FALSE;
$_mmcucn_cache = &drupal_static('_mmcucn_cache');
if (empty($account)) {
$account = \Drupal::currentUser();
}
$perms = [
Constants::MM_PERMS_WRITE => FALSE,
Constants::MM_PERMS_SUB => FALSE,
Constants::MM_PERMS_APPLY => FALSE,
Constants::MM_PERMS_READ => FALSE,
];
// There's a chance that the calls to mm_content_node_access() below can lead
// to a recursive call to this function.
if ($recursive) {
if (empty($mode)) {
return $perms;
}
return FALSE;
}
$recursive = TRUE;
if (is_object($node)) {
if ($node->isNew()) {
$recursive = FALSE;
if (empty($mode)) {
return $perms;
}
return FALSE;
}
$nid = $node->id();
}
else {
$nid = $node;
$node = Node::load($nid);
}
$uid = $account->id();
if (!isset($_mmcucn_cache[$nid][$uid])) {
$cid = ":$nid:$uid";
$cached = _mm_content_access_cache($cid);
if (is_array($cached)) {
$_mmcucn_cache[$nid][$uid] = $cached;
}
else {
$params = [
Constants::MM_GET_TREE_NODE => $node,
Constants::MM_GET_TREE_RETURN_BINS => TRUE,
Constants::MM_GET_TREE_RETURN_PERMS => [Constants::MM_PERMS_WRITE, Constants::MM_PERMS_SUB, Constants::MM_PERMS_APPLY, Constants::MM_PERMS_READ, Constants::MM_PERMS_IS_RECYCLE_BIN],
Constants::MM_GET_TREE_USER => $account,
];
$row = Database::getConnection()->query(mm_content_get_query($params))->fetchObject();
if ($row) {
foreach ((array)$row as $key => $val) {
if (strlen($key) == 1) {
$perms[$key] = $val != 0;
}
}
// mm_content_get_query() only considers the permissions of the page, so
// now consider the node itself.
if ($perms[Constants::MM_PERMS_READ]) {
$has_page_write = $perms[Constants::MM_PERMS_WRITE];
$perms[Constants::MM_PERMS_WRITE] = mm_content_node_access($node, 'update', $account);
}
if (isset($row->recycle_bins) && $row->{Constants::MM_PERMS_IS_RECYCLE_BIN}) {
// Node is in a bin and possibly outside at the same time: if user
// can access it in any bin, allow it here.
if (!$perms[Constants::MM_PERMS_READ]) {
foreach (array_unique(explode(',', $row->recycle_bins)) as $bin) {
if ($perms[Constants::MM_PERMS_READ] = mm_content_user_can_recycle($bin, Constants::MM_PERMS_READ, $account)) {
$perms[Constants::MM_PERMS_APPLY] = TRUE;
break;
}
}
}
}
}
else {
// not assigned to any pages
$perms[Constants::MM_PERMS_READ] = $perms[Constants::MM_PERMS_APPLY] = TRUE;
}
// If the user does not have write permission, consider the appearance schedule
if (empty($has_page_write) && !$perms[Constants::MM_PERMS_WRITE] && !mm_content_node_access($node, 'update', $account)) {
$now = mm_request_time();
$scheduled = $node->isPublished() && (!isset($node->publish_on) || $node->publish_on == 0 || $node->publish_on <= $now) && (!isset($node->unpublish_on) || $node->unpublish_on == 0 || $now < $node->unpublish_on);
$perms[Constants::MM_PERMS_READ] = $perms[Constants::MM_PERMS_READ] && $scheduled; // must use logical && to preserve boolean type
}
$_mmcucn_cache[$nid][$uid] = $perms;
// Don't bother trying to figure out cache intervals when
// publish/unpublish is used.
if (empty($node->__get('publish_on')) && empty($node->__get('unpublish_on'))) {
_mm_content_access_cache($cid, $perms, $uid, $nid, 0);
}
}
}
if (mm_site_is_disabled($account)) {
$_mmcucn_cache[$nid][$uid][Constants::MM_PERMS_WRITE] = $_mmcucn_cache[$nid][$uid][Constants::MM_PERMS_APPLY] = $_mmcucn_cache[$nid][$uid][Constants::MM_PERMS_SUB] = FALSE;
}
$recursive = FALSE;
if (empty($mode)) {
return $_mmcucn_cache[$nid][$uid];
}
return $_mmcucn_cache[$nid][$uid][$mode];
}
/**
* Delete an entry and all of its children
*
* @param int $mmtid
* ID of the entry to delete
* @param bool $delete_nodes
* If TRUE, also delete any nodes using these IDs (TRUE)
* @param bool $allow_non_empty_bin
* If TRUE, allow a non-empty recycle bin at the top level to be deleted
* (FALSE)
* @param int $limit_time
* If non-zero, limit the node deletion process to this number of seconds.
* @return string|void
* An error message, if an error occurs
*/
function mm_content_delete($mmtid, $delete_nodes = TRUE, $allow_non_empty_bin = FALSE, $limit_time = 0) {
$iter = new ContentDeleteIter($delete_nodes, $allow_non_empty_bin, time(), $limit_time);
mm_content_get_tree($mmtid, [
Constants::MM_GET_TREE_RETURN_PERMS => TRUE,
Constants::MM_GET_TREE_ITERATOR => $iter,
Constants::MM_GET_TREE_FILTER_HIDDEN => TRUE,
]);
if (!$iter->count) {
return t('Page not found');
}
$iter->delete();
if ($iter->err) {
return $iter->err;
}
}
/**
* Return the long username ('last, first middle.' or 'first middle last')
* associated with a uid.
*
* @param int $uid
* ID of the user to query
* @param string $order
* Either 'lfm', 'fml', 'lfmu', or 'fmlu' to choose the order. Defaults to
* 'lfmu' [last, first, middle, (username)].
* @param AccountInterface|StdClass $usr
* Optional object, from which the name, pref_lfm, pref_fml, firstname,
* middlename, and lastname fields are used to construct (and cache) the
* output
* @param string $hover
* Optionally return the mouse 'hover' text associated with this user
* @return string|false
* The user's long name or FALSE if not found
*/
function mm_content_uid2name($uid, $order = 'lfmu', $usr = NULL, &$hover = NULL) {
static $drupal_static_fast;
// FIXME: This should probably be reworked to use $usr->getDisplayName and hook_user_format_name_alter().
if (!isset($drupal_static_fast)) {
// This is cumbersome, but assigning to an array is the only way that works.
$drupal_static_fast['cache'] = &drupal_static(__FUNCTION__, []);
}
$cache = &$drupal_static_fast['cache'];
if (!isset($cache[$uid])) {
if ($uid == 0) {
$cache[$uid] = (object) ['name' => mm_get_setting('usernames.anon')];
}
elseif ($uid == 1) {
$cache[$uid] = (object) ['name' => mm_get_setting('usernames.admin')];
}
else {
if (!$usr) {
$usr = User::load($uid);
}
if (!is_object($usr)) {
$cache[$uid] = FALSE;
}
else {
$disabled = class_implements($usr, 'isActive') ? !$usr->isActive() : isset($usr->status) && $usr->status == 0;
mm_module_invoke_all_array('mm_uid2name_alter', [$usr, &$disabled]);
if ($disabled && !\Drupal::currentUser()->hasPermission('administer all users')) {
$cache[$uid] = (object) ['name' => mm_get_setting('usernames.disabled', t('Inactive user'))];
}
else {
$middle = '';
if (isset($usr->middlename) && mb_strlen($usr->middlename)) {
$middle = ' ' . $usr->middlename[0] . '.';
}
$cache[$uid] = (object) [
'pref_fml' => $usr->pref_fml ?? '',
'pref_lfm' => $usr->pref_lfm ?? '',
'last' => $usr->lastname ?? '',
'first' => $usr->firstname ?? '',
'name' => isset($usr->name) ? (is_scalar($usr->name) ? $usr->name : $usr->name->getValue()[0]['value']) : '',
'middle' => $middle,
'hover' => $usr->hover ?? '',
'disabled' => $disabled ? ' ' . t('(inactive)') : '',
];
}
}
}
}
if (($u = $cache[$uid]) !== FALSE) {
$hover = $u->hover ?? '';
$uname_only = FALSE;
$fml = mb_substr($order, 0, 3) == 'fml';
if ($fml ? !empty($u->pref_fml) : !empty($u->pref_lfm)) {
$out = $fml ? $u->pref_fml : $u->pref_lfm;
}
elseif (!empty($u->last) && !empty($u->first)) {
$out = $fml ? "$u->first$u->middle $u->last" : "$u->last, $u->first$u->middle";
}
elseif (!empty($u->last)) {
$out = $u->last;
}
elseif (!empty($u->first)) {
$out = $u->first;
}
else {
$out = $u->name;
$uname_only = TRUE;
}
if (isset($order, $order[3]) && $order[3] == 'u' && $u->name != '' && !$uname_only) {
$out .= " ($u->name)";
}
$out .= $u->disabled ?? '';
return $out;
}
return FALSE;
}
/**
* Return the uid associated with a username
*
* @param string $username
* ID of the user to query
* @return int|false
* The user's ID or FALSE if not found
*/
function mm_content_name2uid($username) {
$mmc_u2uid_cache = &drupal_static(__FUNCTION__, []);
if (!isset($mmc_uid2u_cache[$username])) {
$usr = user_load_by_name($username);
$mmc_u2uid_cache[$username] = is_object($usr) ? $usr->id() : FALSE;
}
return $mmc_u2uid_cache[$username];
}
/**
* Determine if a tree entry is a group
*
* @param int $mmtid
* The numeric ID of the potential group
* @return bool
* TRUE if the group ID is that of an existing (possibly virtual) group, and
* not a different type of tree entry
*/
function mm_content_is_group($mmtid) {
$mmc_isgrp_cache = &drupal_static(__FUNCTION__, []);
if (!isset($mmc_isgrp_cache[$mmtid])) {
$list = mm_content_get_parents_with_self($mmtid, FALSE, TRUE);
$mmc_isgrp_cache[$mmtid] = isset($list[1]) && $list[1] == mm_content_groups_mmtid();
}
return $mmc_isgrp_cache[$mmtid];
}
/**
* Determine if a tree entry is a virtual group
*
* @param int $mmtid
* The numeric ID of the group
* @return bool
* TRUE if the group ID is that of an existing virtual group, and not a
* different type of tree entry
*/
function mm_content_is_vgroup($mmtid) {
$list = mm_content_get_parents_with_self($mmtid);
return count($list) >= 3 && ($tree = mm_content_get($list[2])) && $tree->name == Constants::MM_ENTRY_NAME_VIRTUAL_GROUP;
}
/**
* Determine if a tree ID belongs to a normal entry, as opposed to a group
*
* @param int $mmtid
* The tree ID to test
* @param bool $user_is_normal
* If TRUE, consider anything in .Users to be a normal entry
* @return bool
* TRUE if the ID refers to a normal entry
*/
function mm_content_is_normal($mmtid, $user_is_normal = TRUE) {
$cache = &drupal_static(__FUNCTION__, []);
if (!isset($cache[$mmtid][$user_is_normal])) {
$list = mm_content_get_parents_with_self($mmtid);
if (count($list) == 1) { // root
$cache[$mmtid][$user_is_normal] = FALSE;
}
else {
$cache[$mmtid][$user_is_normal] = $list[1] != mm_content_groups_mmtid() &&
($user_is_normal || $list[1] != mm_content_users_mmtid());
}
}
return $cache[$mmtid][$user_is_normal];
}
/**
* Determine if a tree ID refers to the main page or the archive page of an
* archive
*
* @param int $mmtid
* The tree ID to test
* @param bool $test_main
* If TRUE, see if the ID refers to the main page, otherwise the archive page
* @return bool
* TRUE if the ID refers to an archive page of the requested type
*/
function mm_content_is_archive($mmtid, $test_main = FALSE) {
$tree = mm_content_get($mmtid, Constants::MM_GET_ARCHIVE);
return is_object($tree) && $mmtid == ($test_main ? $tree->main_mmtid : $tree->archive_mmtid);
}
/**
* Determine if a tree entry is a child of another entry; useful in preventing
* bad moves.
*
* @param int $child
* The tree ID of the child entry
* @param int $of
* The tree ID of the entry to test for a relationship
* @return bool
* TRUE if $child is a child of $of
*/
function mm_content_is_child($child, $of) {
return in_array($of, mm_content_get_parents($child));
}
/**
* Determine if a tree entry is in a recycle bin.
*
* @param int $mmtid
* The tree ID of the entry to test
* @return bool
* TRUE if $mmtid is in a bin
*/
function mm_content_is_recycled($mmtid) {
return mm_content_user_can($mmtid, Constants::MM_PERMS_IS_RECYCLED);
}
/**
* Determine if a tree entry is a recycle bin.
*
* @param int $mmtid
* The tree ID of the entry to test
* @return bool
* TRUE if $mmtid is a recycle bin
*/
function mm_content_is_recycle_bin($mmtid) {
return mm_content_user_can($mmtid, Constants::MM_PERMS_IS_RECYCLE_BIN);
}
/**
* Get a list of all blocks
*
* @param bool $allowed
* If set, only return the blocks the current user can apply to a page
* @return Drupal\block\Entity\Block[]
* An array, indexed on block ID, containing Drupal\block\Entity\Block
* elements
*/
function mm_content_get_blocks($allowed = FALSE) {
static $cache;
if (!is_array($cache)) {
$cache = mm_get_mm_blocks();
}
if (!$allowed || \Drupal::currentUser()->hasPermission('administer all menus')) {
return $cache;
}
return array_filter($cache, fn(Block $block) => empty($block->toArray()['settings']['admin_only']));
}
/**
* Search up the path, looking for the bottom-most block with an entry in
* mm_tree_block
*
* @param int[] $mmtids
* List of tree IDs comprising the path to search
* @param string $block_id
* - On entry: Set to the block ID of the block to match, or leave as '' to
* search all blocks
* - On return: If '' on entry, this variable is set to the ID of the block
* that was found
* @param bool $multiple
* If TRUE, return all blocks in the list
* @return mixed
* Tree ID of the starting point, all blocks in the list (if $multiple), or 0
* on error
*/
function mm_content_get_blocks_at_mmtid(array $mmtids, &$block_id = '', $multiple = FALSE) {
// Remove virtual directory
$mmtids = array_filter($mmtids, fn($mmtid) => $mmtid >= 0);
// Reindex
$mmtids = array_values($mmtids);
if (!count($mmtids)) {
return 0;
}
$db = Database::getConnection();
$select = $db->select('mm_tree_block', 'tb');
$select->join('mm_tree', 't', 'tb.mmtid = t.mmtid');
$select->condition('tb.bid', [Constants::MM_MENU_DEFAULT, Constants::MM_MENU_UNSET], 'NOT IN');
$cond = $db->condition('AND');
$cond->condition('t.parent', $mmtids, 'IN');
if ($block_id != Constants::MM_MENU_DEFAULT && $block_id != Constants::MM_MENU_UNSET) {
$cond->condition('tb.bid', (string) $block_id);
}
$or = $db->condition('OR');
$select->fields('t', ['mmtid', 'parent'])
->fields('tb', ['bid', 'max_depth', 'max_parents'])
->condition($or
->condition('t.mmtid', $mmtids, 'IN')
->condition($cond));
$direct = $by_parent = [];
$result = $select->execute();
foreach ($result as $r) {
if (!in_array($r->mmtid, $mmtids)) {
// Starting point of a sub-menu
$by_parent[$r->parent][] = (array)$r;
}
else {
$direct[$r->mmtid] = (array)$r;
}
}
$blocks_with_node_contents = mm_get_mm_blocks(['settings.show_node_contents' => 1]);
if ($direct || $by_parent) {
for ($i = count($mmtids); --$i >= 0;) {
$t = $mmtids[$i];
if (isset($direct[$t])) {
$entry = $direct[$t];
if (!$block_id || $entry['bid'] == $block_id) {
if (!$block_id) {
$block_id = $entry['bid'];
}
if ($multiple) {
return [$entry];
}
return $t;
}
}
if (isset($by_parent[$t])) {
foreach ($by_parent[$t] as $entry) {
if (!$block_id || $entry['bid'] == $block_id) {
// If show_node_contents is set, just go with it.
$ok = isset($blocks_with_node_contents[$entry['bid']]);
// Otherwise, see what block the parent is in.
for ($j = $i; $j >= 0 && !$ok; $j--) {
// If the parent is in a different block, it's OK.
if (isset($direct[$mmtids[$j]])) {
if ($direct[$mmtids[$j]]['bid'] == $entry['bid']) {
// Same block as parent, ignore it.
break;
}
$ok = TRUE;
}
}
if ($ok) {
if (!$block_id) {
$block_id = $entry['bid'];
}
if ($multiple) {
return $by_parent[$t];
}
return $entry['mmtid'];
}
}
}
}
}
}
return 0;
}
/**
* Return the tree ID of the /root/.Groups entry
*
* @return int
* The tree ID
*/
function mm_content_groups_mmtid() {
$mmtid = \Drupal::state()->get('monster_menus.groups_mmtid');
if (empty($mmtid)) {
$tree = mm_content_get(['parent' => 1, 'name' => Constants::MM_ENTRY_NAME_GROUPS]);
\Drupal::state()->set('monster_menus.groups_mmtid', $mmtid = $tree[0]->mmtid);
}
return $mmtid;
}
/**
* Return the tree ID of the /root/.Users entry
*
* @return int
* The tree ID
*/
function mm_content_users_mmtid() {
$mmtid = \Drupal::state()->get('monster_menus.users_mmtid');
if (empty($mmtid)) {
$tree = mm_content_get(['parent' => 1, 'name' => Constants::MM_ENTRY_NAME_USERS]);
\Drupal::state()->set('monster_menus.users_mmtid', $mmtid = $tree[0]->mmtid);
}
return $mmtid;
}
/**
* Return the alias of the /root/.Users entry
*
* @return string
* The alias
*/
function mm_content_users_alias() {
if (is_null($alias = \Drupal::state()->get('monster_menus.users_alias', NULL))) {
if ($tree = mm_content_get(['parent' => 1, 'name' => Constants::MM_ENTRY_NAME_USERS])) {
\Drupal::state()->set('monster_menus.users_alias', $alias = $tree[0]->alias);
}
// Should only happen when uninstalling the module.
return '';
}
return $alias;
}
/**
* Return a list of user IDs in an MM group, query whether a given uid is in one
* or more groups, or return all groups to which a uid belongs. This function
* differs from mm_content_get_uids_in_group(), in that permissions are not
* considered and the user's name is not returned.
*
* @param int|array|null $mmtids
* A single tree ID (gid) or an array of gids (can be empty or NULL)
* @param int|array $uids
* A single user ID, or an array of user IDs to query (optional)
* @param bool $normal
* If TRUE, consider "normal" (not ad-hoc or virtual) groups (optional)
* @param bool $virtual
* If TRUE, consider virtual groups (optional)
* @param bool $ad_hoc
* If TRUE, consider ad-hoc groups (optional)
* @param Connection $database
* (optional) The database connection to use.
* @return array
* If both $mmtids and $uids are set:
* - If $uids is a single value, return an array containing the groups
* matching $mmtids to which the user belongs
* - If $uids is an array, return an array where the key is the uid and the
* value is an array containing the groups matching $mmtids to which the
* user belongs
* If only $mmtids is set:
* - If $mmtids is a single value, return an array containing all uids that
* are members of the group
* - If $mmtids is an array, return an array where the key is the mmtid and
* the value is an array containing all uids that are members of the group
* If only $uids is set:
* - If $uids is a single value, return all mmtids (gids) to which the user
* belongs
* - If $uids is an array, return an array where the key is the uid and the
* value is an array containing all mmtids (gids) to which the user belongs
*
* At least one of $normal, $virtual, or $ad_hoc must be TRUE.
*/
function mm_content_get_uids_in_group($mmtids, $uids = NULL, $normal = TRUE, $virtual = TRUE, $ad_hoc = TRUE, Connection $database = NULL) {
if (!$virtual && !$normal && !$ad_hoc) {
return [];
}
if (!empty($mmtids)) {
$in_mmtids = 'IN (:mmtids[])';
if (!is_array($mmtids)) {
$mmtids_single = TRUE;
}
}
if (!empty($uids)) {
$in_uids = 'IN (:uids[])';
if (!is_array($uids)) {
$uids = [$uids];
$uids_single = TRUE;
}
// uid=0 should never appear in any group
$uids = array_diff($uids, [0]);
if (!$uids) {
return [];
}
}
else if (isset($uids)) {
// Just the UID 0, not as an array.
return [];
}
$out = [];
if (!empty($mmtids) && !empty($uids)) {
$where1 = "g.gid $in_mmtids AND v.uid $in_uids";
$where2 = "gid $in_mmtids AND uid $in_uids";
$params = [':mmtids[]' => (array) $mmtids, ':uids[]' => $uids];
}
elseif (!empty($mmtids)) {
$where1 = "g.gid $in_mmtids";
$where2 = "gid $in_mmtids AND uid > 0";
$params = [':mmtids[]' => (array) $mmtids];
}
elseif (!empty($uids)) {
$where1 = "v.uid $in_uids";
$where2 = "uid $in_uids";
$params = [':uids[]' => $uids];
}
else {
return [];
}
$qs = [];
if ($virtual) {
$qs[] = 'SELECT g.gid, v.uid FROM {mm_group} g ' .
'INNER JOIN {mm_virtual_group} v ON v.vgid = g.vgid ' .
"WHERE $where1";
}
if ($normal || $ad_hoc) {
if (!$normal) $where2 .= ' AND gid < 0';
elseif (!$ad_hoc) $where2 .= ' AND gid > 0';
$qs[] = 'SELECT gid, uid FROM {mm_group} ' .
"WHERE $where2";
}
$query = mm_retry_query(join(' UNION ', $qs), $params, [], $database);
if (!empty($uids)) {
foreach ($query as $row) {
$out[$row->uid][] = $row->gid;
}
if (!empty($uids_single)) {
return $out[$uids[0]] ?? [];
}
return $out;
}
foreach ($query as $row) {
$out[$row->gid][] = $row->uid;
}
if (!empty($mmtids_single)) {
return $out[$mmtids] ?? [];
}
return $out;
}
/**
* Return a list of users in an MM group, suitable for presentation in the UI.
* This function calls hook_mm_get_users_in_group_alter(), which can be used to
* prevent the disclosure of group membership to unauthorized viewers.
*
* @param int|array $mmtid
* Tree ID (gid) of the group
* @param string $sep
* If not set, return an array. If set, join the list of users with this
* string and return the result.
* @param bool $halt
* If TRUE, and there are more than $limit matches, return NULL
* @param int $limit
* If non-zero, limit the number of results. If the number of results would
* exceed the limit, either append '...' to the return (when a string), or add
* a '...' element to the returned array (when an array is requested).
* @param bool $see_all
* If TRUE, and there are more than $limit matches, include a "see all users"
* link
* @param array $render_array
* If $see_all is TRUE, this parameter must contain a reference to the render
* array being returned in the response. If there are more than $limit users,
* it will be modified to include the necessary Javascript libraries to
* display the full list in a modal dialog.
* @return string | array
* The textual or array list, or possibly NULL if $halt is set
*/
function mm_content_get_users_in_group($mmtid, $sep = NULL, $halt = FALSE, $limit = 20, $see_all = FALSE, &$render_array = []) {
$mmtids = is_array($mmtid) ? $mmtid : [$mmtid];
foreach ($mmtids as $test_mmtid) {
if (!mm_content_user_can($test_mmtid, Constants::MM_PERMS_READ)) {
$msg = t('(not permitted to see list)');
if (isset($sep)) {
return $msg;
}
return [$msg];
}
}
$limit_str = $limit ? "AND v.preview <= $limit + 1 " : '';
$lim = '';
$db = Database::getConnection();
$mmtid_match = 'IN(' . join(', ', $mmtids) . ')';
$qs = 'SELECT %s FROM ((SELECT %s FROM ' .
'(SELECT u.uid, u.name FROM ' .
"(SELECT * FROM {mm_group} WHERE gid $mmtid_match) AS g " .
"INNER JOIN {mm_virtual_group} v ON v.vgid = g.vgid $limit_str" .
'INNER JOIN {users_field_data} u ON u.uid = v.uid) AS u) ' .
'UNION ' .
'(SELECT %s FROM ' .
'(SELECT u.uid, u.name FROM ' .
"(SELECT * FROM {mm_group} WHERE gid $mmtid_match AND uid > 0) AS g " .
'INNER JOIN {users_field_data} u ON u.uid = g.uid) AS u)) x';
$query = sprintf($qs . ' ORDER BY x.name', '*', 'u.uid, u.name', 'u.uid, u.name');
$countquery = sprintf($qs, 'SUM(c)', 'COUNT(DISTINCT u.uid) AS c', 'COUNT(DISTINCT u.uid) AS c', '');
mm_module_invoke_all_array('mm_get_users_in_group_alter', [$mmtids, &$query, &$countquery]);
if ($limit > 0) {
if ($halt) {
$r = $db->query($countquery)->fetchField();
if ($r == NULL || $r > $limit) {
return NULL;
}
}
else {
$lim .= ' LIMIT ' . ($limit + 1);
}
}
$query .= $lim;
$q = mm_retry_query($query);
$users = [];
foreach ($q as $r) {
$users[$r->uid] = mm_content_uid2name($r->uid, 'lfmu', $r);
}
if (!$halt && $limit > 0 && count($users) == $limit + 1 && count($mmtids) == 1) {
$overflow = TRUE;
array_pop($users);
if ($see_all && $mmtids[0] > 0) {
$tree = mm_content_get($mmtids[0]);
$see_link = Link::fromTextAndUrl(
t('See all users in this group'),
Url::fromRoute('monster_menus.show_group', ['mm_tree' => $mmtids[0]],
['attributes' => [
// The content of the iframe is loaded quite a while after the
// dialog opens, so set a minHeight to get it to center correctly.
'id' => mm_ui_modal_dialog(['iframe' => TRUE, 'minWidth' => 400, 'minHeight' => 570], $render_array),
'title' => t('All users in the group @name', ['@name' => mm_content_get_name($tree)]),
]])
)->toString();
$users = array_merge([-1 => $see_link], $users);
}
}
if (!empty($overflow)) {
$users[''] = '...';
}
if (!isset($sep)) {
return $users;
}
return implode($sep, $users);
}
/**
* Ensure that a particular alias will not conflict with the core menu tree, or
* other entries already at the same level, by adding "_N" to it until it no
* longer conflicts.
*
* @param string $alias
* The initial alias
* @param int $mmtid
* The tree ID of the parent entry
* @return string
* The safe alias
*/
function mm_content_get_safe_alias($alias, $mmtid) {
$i = 0;
$test = $alias;
while (mm_content_alias_conflicts($test, $mmtid, TRUE) || mm_content_alias_exists($alias, $mmtid)) {
$test = $alias . '_' . (++$i);
}
return $test;
}
/**
* Get the list of words that can never be used as URL aliases.
*
* @return array
* An array containing the list of words
*/
function mm_content_reserved_aliases() {
return \Drupal::state()->get('monster_menus.reserved_alias', mm_content_reserved_aliases_base());
}
/**
* Get the base list of words that can never be used as URL aliases. This is
* before any words that are added based on Drupal menu entries.
*
* @return array
* An array containing the list of words
*/
function mm_content_reserved_aliases_base() {
return ['feed'];
}
/**
* Ensure that a particular alias will not conflict with the core menu tree
* and/or one of the reserved aliases.
*
* @param string $alias
* The initial alias
* @param int $mmtid
* The tree ID of the parent entry
* @param bool $check_reserved
* If TRUE, check against mm_content_reserved_aliases() list and core menus;
* otherwise just core menus
* @return bool
* TRUE if the alias conflicts
*/
function mm_content_alias_conflicts($alias, $mmtid, $check_reserved = TRUE) {
return
$check_reserved && in_array($alias, mm_content_reserved_aliases()) ||
in_array($alias, \Drupal::state()->get('monster_menus.top_level_reserved', [])) && $mmtid == mm_home_mmtid();
}
/**
* Ensure that a particular alias does not already exist at a particular level
* of the tree.
*
* @param string $alias
* The initial alias
* @param int $mmtid
* The tree ID of the parent entry
* @return bool
* TRUE if a tree entry with the alias already exists
*/
function mm_content_alias_exists($alias, $mmtid) {
return (bool) mm_content_get(['parent' => $mmtid, 'alias' => $alias]);
}
/**
* Create a user's home directory in the MM tree.
*
* @param AccountInterface $account
* The user object describing the account being added.
* @return RedirectResponse|void
* The response object which redirects the user to the new home directory.
* @throws NotFoundHttpException
*/
function mm_content_create_homepage(AccountInterface $account) {
if (!empty($account->user_mmtid) && ($perms = mm_content_user_can($account->user_mmtid)) && $perms[Constants::MM_PERMS_IS_RECYCLED]) {
// Restore a page that is in the recycle bin.
mm_content_move_from_bin($account->user_mmtid);
}
else {
if (($exists = mm_content_get($account->user_mmtid, Constants::MM_GET_FLAGS)) && ($parent = mm_content_get($exists->parent)) && $parent->name == Constants::MM_ENTRY_NAME_DISABLED_USER) {
// Unset flag in a homepage that is in the "disabled" tree.
unset($exists->flags['user_home']);
mm_content_set_flags($exists->mmtid, $exists->flags);
$account->user_mmtid = NULL;
}
// Create from scratch.
mm_content_add_user($account);
}
if ($account->user_mmtid) {
return new RedirectResponse(mm_content_get_mmtid_url($account->user_mmtid, ['absolute' => TRUE])->toString());
}
throw new NotFoundHttpException();
}
/**
* Create a user's home directory in the MM tree.
*
* @param AccountInterface $account
* The user object describing the account being added
*/
function mm_content_add_user(AccountInterface $account) {
if (!empty($account->user_mmtid) || !mm_get_setting('user_homepages.enable')) {
return;
}
$users_mmtid = mm_content_users_mmtid();
$default_tree = mm_content_get(['name' => Constants::MM_ENTRY_NAME_DEFAULT_USER, 'parent' => $users_mmtid]);
if (!count($default_tree)) {
\Drupal::logger('user')->error('Missing @path', ['@path' => '/' . Constants::MM_ENTRY_NAME_USERS . '/' . Constants::MM_ENTRY_NAME_DEFAULT_USER]);
return;
}
$fullname = mm_content_uid2name($account->id(), 'lfmu', NULL, $hover);
$dest_mmtid = $users_mmtid;
foreach (mm_module_implements('mm_add_user_alter') as $function) {
if ($function($account, $dest_mmtid, $fullname) === FALSE) {
return;
}
}
// Find the first empty slot with a name ending in either $name or
// '$name (username)'
for ($i = 0;;) {
$test = !$i ? preg_replace('/ \(.*?\)$/', '', $fullname) : $fullname;
$exists = mm_content_get(['name' => $test, 'parent' => $users_mmtid]);
if (!count($exists)) {
$name = $test;
break;
}
$other = User::load($exists[0]->uid);
// If owner of homedir no longer exists or his username is the same as
// the one on the new account, move the old homedir out of the way.
if ($other === FALSE || $other->getAccountName() == $account->getAccountName()) {
$name = $test;
if (($err = mm_content_move_to_disabled($exists[0]->mmtid)) !== FALSE) {
\Drupal::logger('user')->error('Error moving existing account %name into @path: @message', ['%name' => $name, '@path' => Constants::MM_ENTRY_NAME_DISABLED_USER, '@message' => $err]);
return;
}
else {
\Drupal::logger('user')->warning('Moved existing account %name into @path', ['%name' => $name, '@path' => Constants::MM_ENTRY_NAME_DISABLED_USER]);
}
break;
}
if ($i++) {
\Drupal::logger('user')->error('Could not create %test homepage because it already exists', ['%test' => $test]);
return;
}
}
$copy_params = [
Constants::MM_COPY_ALIAS => mm_content_get_safe_alias($account->getAccountName(), $dest_mmtid),
Constants::MM_COPY_CONTENTS => TRUE,
Constants::MM_COPY_NAME => $name,
Constants::MM_COPY_OWNER => $account->id(),
];
$new_mmtid = mm_content_copy($default_tree[0]->mmtid, $dest_mmtid, $copy_params);
if (is_numeric($new_mmtid)) {
$account->user_mmtid = $new_mmtid;
mm_content_set_flags($new_mmtid, ['user_home' => $account->id()], FALSE);
mm_content_update_quick(['hover' => $hover], ['mmtid' => $new_mmtid]);
mm_module_invoke_all_array('mm_add_user_post', [&$account, $new_mmtid, $dest_mmtid]);
}
else {
\Drupal::logger('user')->error('Error copying default user home dir into %name: @message', ['%name' => $name, '@message' => $new_mmtid]);
}
mm_content_clear_caches($users_mmtid);
}
/**
* Get a URL for the MM path of the current page. This lets you generate URLs
* that preserve the MM menu state.
*
* @param string $rel_url
* An optional relative path to append to the current path.
* @param array $options
* An array of options for the new Url object.
* @return Url
* The new URL object
*/
function mm_content_current_mm_url($rel_url = NULL, $options = []) {
$url = mm_get_current_path();
if ($rel_url) {
$url .= '/' . $rel_url;
$mmtids = $oargs = [];
$this_mmtid = \Drupal::service('monster_menus.path_processor_inbound')
->getMmtidOfPath($url, $mmtids, $oargs);
}
else {
mm_parse_args($mmtids, $oarg_list, $this_mmtid, $url);
}
if (is_null($this_mmtid)) {
$this_mmtid = mm_home_mmtid();
}
return mm_content_get_mmtid_url($this_mmtid, $options);
}
/**
* Copy an item (and, optionally, its children) within the MM tree.
*
* @param int $src_mmtid
* Tree ID of the entry to start copying from
* @param int $dest_mmtid
* Tree ID of the entry to copy to
* @param array $options
* An array containing options. The array is indexed using the constants
* below.
* - MM_COPY_ALIAS (NULL):
* URL alias of the new item, or NULL to keep the original value
* - MM_COPY_COMMENTS (FALSE):
* If TRUE, and MM_COPY_CONTENTS is also TRUE, copy the comments associated
* with any contents
* - MM_COPY_CONTENTS (FALSE):
* If TRUE, copy the contents of the page(s)
* - MM_COPY_ITERATE_ALTER (none):
* If set, this function or array of functions is called before any
* processing is done on each entry in the tree. If the function returns -1,
* the entry and any children will be skipped; if it returns 1, just the
* current entry is skipped; if it returns 0, all further processing is
* canceled; any other return value leads to no change. The function can
* also alter the item passed to it.
* - MM_COPY_NAME (NULL):
* Name of the new, top-level item, or NULL to keep the original value
* - MM_COPY_NODE_PRESAVE_ALTER (none):
* If set, this function or array of functions is passed copied nodes just
* before creation, for possible alteration
* - MM_COPY_OWNER (no change):
* If set, change the owner of the copies to this UID
* - MM_COPY_READABLE (FALSE):
* If TRUE, only copy entries readable by the user
* - MM_COPY_RECUR (TRUE):
* If TRUE, copy recursively (include all children)
* - MM_COPY_TREE (TRUE):
* If TRUE, copy the page(s)
* - MM_COPY_TREE_PRESAVE_ALTER (none):
* If set, this function or array of functions is passed the new page's
* description just before creation, for possible alteration
* - MM_COPY_TREE_SKIP_DUPS (FALSE):
* If TRUE, a check is done to ensure that tree entries with the same
* aliases as existing entries are not created in the destination
* @return int|string
* If successful, the tree ID of the first, new entry; otherwise, a human-
* readable error message
*/
function mm_content_copy($src_mmtid, $dest_mmtid, $options) {
$iter = new ContentCopyIter($src_mmtid, $dest_mmtid, $options);
if ($iter->options[Constants::MM_COPY_RECUR]) {
if ($msg = _mm_content_test_copy_move($src_mmtid, $dest_mmtid)) {
return $msg;
}
}
$params = [
Constants::MM_GET_TREE_DEPTH => $iter->options[Constants::MM_COPY_RECUR] ? -1 : 0,
Constants::MM_GET_TREE_ITERATOR => $iter,
Constants::MM_GET_TREE_RETURN_PERMS => $iter->options[Constants::MM_COPY_READABLE] ? TRUE : NULL,
Constants::MM_GET_TREE_RETURN_FLAGS => TRUE,
];
mm_content_get_tree($src_mmtid, $params);
mm_content_clear_caches($dest_mmtid);
\Drupal::logger('mm')->notice('Copied mmtid=@src to parent=@dest@recur, new name=%name (%alias)', ['@src' => $src_mmtid, '@dest' => $dest_mmtid, '@recur' => $iter->options[Constants::MM_COPY_RECUR] ? ' ' . t('recursively') : '', '%name' => $iter->options[Constants::MM_COPY_NAME], '%alias' => $iter->options[Constants::MM_COPY_ALIAS]]);
return $iter->output();
}
/**
* Move an entry within the MM tree.
*
* @param int $src_mmtid
* Tree ID of the entry to move
* @param int $dest_mmtid
* Tree ID of the destination entry (new parent)
* @param string $recycle_mode
* Set to either 'recycle' or 'restore' to indicate if we are manipulating the
* recycle bin.
* @return bool|string
* FALSE if successful; otherwise, a human-readable error message
*/
function mm_content_move($src_mmtid, $dest_mmtid, $recycle_mode = '') {
if ($msg = _mm_content_test_copy_move($src_mmtid, $dest_mmtid)) {
return $msg;
}
$src = mm_content_get($src_mmtid);
$list = mm_content_get_parents_with_self($dest_mmtid, FALSE, FALSE); // don't include virtual parents
$bin = $recycle_mode == 'recycle' ? $dest_mmtid : mm_content_get_parent($src_mmtid);
$iter = new ContentMoveIter($list, $recycle_mode, $bin, $src->sort_idx);
$params = [
Constants::MM_GET_TREE_RETURN_PERMS => $recycle_mode == 'recycle' ? TRUE : NULL,
Constants::MM_GET_TREE_DEPTH => -1,
Constants::MM_GET_TREE_ITERATOR => $iter,
];
mm_content_get_tree($src_mmtid, $params);
mm_content_clear_caches([$src_mmtid, $dest_mmtid]);
foreach (array_keys($iter->delete_bins) as $bin) {
mm_content_delete_bin($bin);
}
$vals = ['@src' => $src_mmtid, '@dest' => $dest_mmtid];
switch ($recycle_mode) {
case 'recycle':
\Drupal::logger('mm')->notice('Recycled mmtid=@src to bin=@dest', $vals);
break;
case 'restore':
mm_content_clear_caches($iter->bin);
\Drupal::logger('mm')->notice('Restored mmtid=@src to parent=@dest', $vals);
break;
default:
\Drupal::logger('mm')->notice('Moved mmtid=@src to new parent=@dest', $vals);
}
if ($iter->error) {
\Drupal::logger('mm')->error('Move error: %error', ['%error' => $iter->error]);
return $iter->error;
}
return FALSE;
}
/**
* Move a user's home directory into /.Users/.Disabled within the MM tree.
*
* @param int $mmtid
* Tree ID of the entry to move
* @return bool|string
* FALSE if successful; otherwise, a human-readable error message
*/
function mm_content_move_to_disabled($mmtid) {
$user_dir = mm_content_get(['name' => Constants::MM_ENTRY_NAME_USERS, 'parent' => 1]);
if (!$user_dir) {
return t('@dir not found', ['@dir' => Constants::MM_ENTRY_NAME_USERS]);
}
$disab_dir = mm_content_get(['name' => Constants::MM_ENTRY_NAME_DISABLED_USER, 'parent' => $user_dir[0]->mmtid]);
if (!$disab_dir) {
return t('@dir not found', ['@dir' => Constants::MM_ENTRY_NAME_DISABLED_USER]);
}
// This might create duplicate names in /.Users/.Disabled, but it's probably
// best to leave them.
return mm_content_move($mmtid, $disab_dir[0]->mmtid);
}
function _mm_content_get_next_sort($parent) {
$db = Database::getConnection();
$max = $db->select('mm_tree', 't')
->fields('t', ['sort_idx'])
->condition('t.parent', $parent)
->orderBy('t.sort_idx', 'DESC')
->range(0, 1)
->execute()->fetchField();
if (empty($max)) {
$parent_sort_idx = $db->select('mm_tree', 't')
->fields('t', ['sort_idx'])
->condition('t.mmtid', $parent)
->execute()->fetchField();
return $parent_sort_idx . _mm_content_btoa(0);
}
return substr($max, 0, -Constants::MM_CONTENT_BTOA_CHARS) . _mm_content_btoa(_mm_content_atob(substr($max, -Constants::MM_CONTENT_BTOA_CHARS)) + 1);
}
function _mm_content_test_sort_length($sort_idx, $msg, $critical = FALSE) {
static $max, $did_error;
if (empty($max)) {
$max = \Drupal::service('entity_field.manager')->getActiveFieldStorageDefinitions('mm_tree')['sort_idx']->getSetting('max_length');
}
if (strlen($sort_idx) > $max) {
if (empty($did_error)) {
if (is_numeric($msg)) {
$vars = ['@mmtid' => $msg];
$msg = 'The tree is nested too deeply, starting at mmtid=@mmtid. Presentation of sorted trees will suffer. To correct this problem, increase the length of mm_tree.sort_idx then run mm_content_update_sort().';
}
else {
$vars = $msg[1];
$msg = $msg[0];
}
$critical ? \Drupal::logger('mm')->critical($msg, $vars) : \Drupal::logger('mm')->notice($msg, $vars);
$did_error = TRUE;
}
return FALSE;
}
return TRUE;
}
function _mm_content_test_copy_move($src_mmtid, $dest_mmtid) {
// Don't allow a copy/move to happen if the resulting sort_idx would be too long
$max_sort_idx = Database::getConnection()->query('SELECT ' .
'CONCAT(' .
'(SELECT sort_idx FROM {mm_tree} WHERE mmtid = :dest_mmtid), ' .
'SUBSTR(' .
"REPEAT('x', " .
'(SELECT MAX(LENGTH(t.sort_idx)) ' .
'FROM {mm_tree_parents} p ' .
'INNER JOIN {mm_tree} t ON t.mmtid = p.mmtid ' .
'WHERE p.parent = :src_mmtid1 OR t.mmtid = :src_mmtid2' .
')' .
'), ' .
'(SELECT LENGTH(sort_idx) ' .
'FROM {mm_tree} WHERE mmtid = :src_mmtid3' .
') - :length' .
')' .
')',
[
':dest_mmtid' => $dest_mmtid,
':src_mmtid1' => $src_mmtid,
':src_mmtid2' => $src_mmtid,
':src_mmtid3' => $src_mmtid,
':length' => Constants::MM_CONTENT_BTOA_CHARS - 1,
]
)->fetchField();
$msg = ['An attempt to copy or move mmtid=@src to mmtid=@dest failed, because it would result in a tree that is too deeply nested. To correct this problem, increase the length of mm_tree.sort_idx then run mm_content_update_sort().', ['@src' => $src_mmtid, '@dest' => $dest_mmtid]];
if (!_mm_content_test_sort_length($max_sort_idx, $msg)) {
return t('This operation cannot be performed because it would cause the tree to become too deeply nested. Please contact a system administrator.');
}
return FALSE;
}
/**
* Add a new entry or replace an existing entry in the MM tree.
*
* @param bool $add
* TRUE if the entry is new
* @param int $mmtid
* Tree ID of the new entry's parent ($add=TRUE), or the ID of the entry to
* replace
* @param array|object $parameters
* Either an array or an object with one or more of the attributes listed
* below. Other modules can add settings using hook_mm_cascaded_settings().
* - 'alias':
* The entry's alias
* - 'archive_mmtid':
* If non-zero, the MM Tree ID of the page containing the archive contents.
* See also: frequency, main_nodes
* - 'cascaded':
* An array containing these possible elements, which describe settings that
* are cascaded downward in the tree to any children:
* - 'allowed_node_types':
* If not NULL, then set the entry's allowed node type list to this array
* of values. To indicate that the parent settings should be inherited,
* pass an empty array or NULL. To indicate that there should be no node
* types allowed, pass array('').
* - 'allow_reorder':
* Allow users with write access to reorder the menu of this page and its
* children (-1 = inherit)
* - 'allowed_themes':
* If not NULL, then set the entry's allowed theme list to this array of
* values
* - 'comments_readable':
* Default comment readability for new nodes added to this entry
* - 'hide_menu_tabs':
* Hide the Contents/Settings/etc. tabs from non-admin users
* (-1 = inherit)
* - 'comment':
* Default comment mode for new content added to this entry (0)
* - 'default_mode':
* Default access mode for the entry, a comma-separated list of
* MM_PERMS_READ, MM_PERMS_WRITE, MM_PERMS_SUB, MM_PERMS_APPLY ('')
* - 'flags':
* A single string or array of strings in flag => value format to be added
* to the entry (admins. only)
* - 'frequency':
* The time division for the archive. Must be one of 'day', 'week', 'month'
* or 'year'.
* - 'hidden':
* If set, the entry only appears in menus if the user can edit it or add
* content to it (FALSE)
* - 'hover':
* Text for title attribute, displayed when the mouse hovers over the link
* ('')
* - 'main_nodes':
* The number of pieces of content to show on the main page of an archive
* - 'max_depth':
* If $menu_start is set, the maximum depth of the menu block (-1)
* - 'max_parents':
* If $menu_start is set, the maximum number of parent levels to display
* (-1)
* - 'members':
* For non-virtual groups, an array of the uids of the group's members; if
* an empty string (''), do not change any existing users
* - 'menu_start':
* If non-zero, a menu block using this block ID starts at this level ('')
* - 'name':
* The entry's name
* - 'node_info':
* The default value for showing 'Submitted by' lines on nodes added to this
* page (TRUE)
* - 'perms':
* An array of arrays [MM_PERMS_READ, MM_PERMS_WRITE, MM_PERMS_SUB,
* MM_PERMS_APPLY]['groups', 'users']. All are optional. For 'groups',
* provide an array of gids; for 'users' an array of uids.
* - 'previews':
* If set, show all nodes on the page as teasers (FALSE)
* - 'propagate_node_perms':
* If TRUE, and !$add, set the permissions on all nodes for which the user
* has write access to match the entry. If recurs_perms is also true,
* recursively set the permissions on the nodes on all children as well.
* (FALSE)
* - 'qfield':
* For a virtual group, the "column to select" portion of the query; ignored
* for non-virtual groups
* - 'qfrom':
* For a virtual group, the "FROM clause" portion of the query; ignored for
* non-virtual groups
* - 'recurs_perms':
* If TRUE, and !$add, recursively set the permissions on all children for
* which the user has write access to match the parent. (FALSE)
* - 'rss':
* If set, show an 'Add this page to my portal' button on the page (FALSE)
* - 'theme':
* Name of the entry's theme, if any (none)
* - 'uid':
* User ID of the entry's owner (1)
* - 'weight':
* Order of the entry among its siblings (0)
* @param array|string $stats
* (optional) Array with which to populate statistics:
* - pages:
* An array indexed by mmtid, containing an array of sub-arrays each with
* the elements "message" and "vars", which describe the pages that were
* acted upon.
* - groups:
* An array indexed by mmtid, containing an array of sub-arrays each with
* the elements "message" and "vars", which describe the groups that were
* acted upon.
* - errors:
* An array containing sub-arrays with the elements "message" and "vars",
* which describe any errors that occurred.
* A count of the number of pages acted upon can be derived using the count()
* function.
* @return
* Tree ID of the entry that was added or replaced, or 0 on error
* @throws \Exception
* Any exception occurring during the insert/update
*/
function mm_content_insert_or_update($add, $mmtid, $parameters, &$stats = 'undef') {
static $defaults = [
'alias' => '',
'archive_mmtid' => 0,
'cascaded' => [],
'comment' => 0,
'default_mode' => '',
'flags' => '',
'frequency' => '',
'hidden' => FALSE,
'hover' => '',
'large_group_form_token' => '',
'main_nodes' => 10,
'max_depth' => -1,
'max_parents' => -1,
'members' => '',
'menu_start' => Constants::MM_MENU_UNSET,
'name' => '',
'node_info' => TRUE,
'perms' => [],
'previews' => FALSE,
'propagate_node_perms' => FALSE,
'qfield' => '',
'qfrom' => '',
'recurs_perms' => FALSE,
'rss' => FALSE,
'theme' => '',
'uid' => 1,
'weight' => 0,
];
static $cascaded_settings;
$parameters = (array)$parameters;
foreach (array_keys($parameters) as $p) {
if (!isset($defaults[$p])) {
\Drupal::logger('mm')->critical('Unknown parameter %name to mm_content_insert_or_update', ['%name' => $p]);
\Drupal::messenger()->addStatus(t('An error occurred.'));
return 0;
}
if ($p == 'cascaded') {
if (!isset($cascaded_settings)) {
$cascaded_settings = mm_content_get_cascaded_settings();
}
foreach (array_keys($parameters[$p]) as $c) {
if (!isset($cascaded_settings[$c])) {
\Drupal::logger('mm')->critical('Unknown cascaded setting %name in mm_content_insert_or_update', ['%name' => $c]);
\Drupal::messenger()->addStatus(t('An error occurred.'));
return 0;
}
}
}
}
$parameters = array_merge($defaults, $parameters);
$is_group = mm_content_is_group($mmtid);
$database = Database::getConnection();
if ($add) {
$parameters['parent'] = $mmtid;
try {
$new = MMTree::create($parameters);
// Note: save() automatically writes a revision.
$new->save();
$mmtid = $new->id();
mm_content_notify_change('insert_page', $mmtid, NULL, $parameters);
_mm_report_stat($is_group, $mmtid, 'Added %name (%alias) mmtid=@mmtid', ['%name' => $parameters['name'], '%alias' => $parameters['alias']], $stats, TRUE);
}
catch (\Exception $e) {
mm_watchdog_exception('mm', $e);
throw $e;
}
}
else {
if ($parameters['recurs_perms']) {
$list = [];
foreach (mm_content_get_tree($mmtid, [Constants::MM_GET_TREE_RETURN_PERMS => TRUE]) as $t) {
if ($t->perms[Constants::MM_PERMS_WRITE]) {
$list[] = $t->mmtid;
}
if (!isset($old)) {
$old = $t;
}
}
}
else {
$list = [$mmtid];
$old = mm_content_get($mmtid);
}
$count = 0;
foreach ($list as $t) {
unset($parameters['sort_idx_dirty']);
$transaction = $database->startTransaction();
try {
if ($count++) { // recursive: item after the first
unset($parameters['parent']);
$updated_rows = $database->update('mm_tree')
->fields(['default_mode' => $parameters['default_mode'], 'uid' => $parameters['uid']])
->condition('mmtid', $t)
->execute();
if ($updated_rows) {
mm_content_write_revision($t);
mm_content_set_perms($t, $parameters['perms'], $is_group, TRUE);
mm_content_clear_routing_cache_tagged($t);
}
}
else { // first item only
$parameters['parent'] = $old->parent;
$parameters['mmtid'] = $parameters['id'] = $t;
$parameters['sort_idx'] = $old->sort_idx;
$parameters['sort_idx_dirty'] = $old->sort_idx_dirty;
$parameters['ctime'] = $old->ctime;
$parameters['cuid'] = $old->cuid;
if ($tree = MMTree::load($t)) {
foreach (array_intersect_key($parameters, $tree->getFields()) as $f => $v) {
$tree->set($f, $v);
}
$tree->setExtendedSettings(array_intersect_key($parameters, $tree->getExtendedSettings()));
$tree->setNewRevision();
$tree->setChangedTime(0);
}
else {
$tree = MMTree::create($parameters);
}
// Note: save() automatically writes a revision.
$tree
->setOldSortValues($old->name, $old->weight, $old->hidden, $old->parent)
->enforceIsNew(FALSE)
->save();
}
mm_content_notify_change('update_page', $t, NULL, $parameters);
// Copy the permissions onto nodes attached to the entry if requested.
if ($parameters['propagate_node_perms']) {
/** @var NodeInterface $node */
foreach (Node::loadMultiple(mm_content_get_nids_by_mmtid($t)) as $node) {
if ($node && $node->access('update')) {
$node->__set('users_w', is_array($parameters['perms'][Constants::MM_PERMS_WRITE]['users']) ? array_flip($parameters['perms'][Constants::MM_PERMS_WRITE]['users']) : []);
if (is_array($parameters['perms'][Constants::MM_PERMS_APPLY]['users'])) {
$node->__set('users_w', $node->__get('users_w') + array_flip($parameters['perms'][Constants::MM_PERMS_APPLY]['users']));
}
$node->__set('groups_w', is_array($parameters['perms'][Constants::MM_PERMS_WRITE]['groups']) ? array_flip($parameters['perms'][Constants::MM_PERMS_WRITE]['groups']) : []);
if (is_array($parameters['perms'][Constants::MM_PERMS_APPLY]['groups'])) {
$node->__set('groups_w', $node->__get('groups_w') + array_flip($parameters['perms'][Constants::MM_PERMS_APPLY]['groups']));
}
$node->__set('others_w', str_contains($parameters['default_mode'], Constants::MM_PERMS_WRITE) || str_contains($parameters['default_mode'], Constants::MM_PERMS_APPLY));
mm_content_set_node_perms($node);
}
}
}
}
catch (\Exception $e) {
$transaction->rollBack();
mm_watchdog_exception('mm', $e);
throw $e;
}
}
mm_content_clear_caches(); // clear everything, because there may be affected kids, even without recursion
$did_perms = [];
if ($parameters['recurs_perms']) {
$did_perms[] = ' ' . t('Permissions were copied to all children.');
}
if ($parameters['propagate_node_perms']) {
$did_perms[] = ' ' . t('Permissions were copied to nodes.');
}
_mm_report_stat($is_group, $mmtid, 'Updated %name (%alias) mmtid = @mmtid.@perms', ['%name' => $parameters['name'], '%alias' => $parameters['alias'], '@perms' => join('', $did_perms)], $stats, TRUE);
}
return $mmtid;
}
/**
* Update an existing entry in the MM tree. Only attributes which are stored in
* the mm_tree table are supported.
*
* @param array|object $parameters
* Either an array or an object with one or more of the attributes listed
* below. Only those attributes which are listed can be updated.
* - 'alias':
* The entry's alias
* - 'comment':
* Default comment mode for new content added to this entry
* - 'default_mode':
* Default access mode for the entry, a comma-separated list of
* MM_PERMS_READ, MM_PERMS_WRITE, MM_PERMS_SUB, MM_PERMS_APPLY
* - 'hidden':
* If set, the entry only appears in menus if the user can edit it or add
* content to it
* - 'hover':
* Text for title attribute, displayed when the mouse hovers over the link
* - 'name':
* The entry's name
* - 'node_info':
* The default value for showing 'Submitted by' lines on nodes added to this
* page
* - 'previews':
* If set, show all nodes on the page as teasers
* - 'rss':
* If set, show an 'Add this page to my portal' button on the page
* - 'theme':
* Name of the entry's theme, if any
* - 'uid':
* User ID of the entry's owner
* - 'weight':
* Order of the entry among its siblings
* @param int|array $where
* Defines what tree entries to update. If a single value is supplied, then it
* is treated as the tree ID (mmtid) of an entry.
*
* An associative array can also be supplied; it must contain one or more
* attributes from the list above, or "mmtid", as the array key. The array
* value becomes part of the update query WHERE. At a minimum, "mmtid" must be
* specified.
* @param int|null $parent
* If the name, hidden, or weight attribute is being changed, the parent tree
* ID may need to be queued for later update. If it is needed and not
* specified in $parent, it will be retrieved using an additional query.
* @param bool $revision
* If TRUE, update the mm_tree_revision table
*/
function mm_content_update_quick($parameters, $where, $parent = NULL, $revision = TRUE) {
$parameters = (array)$parameters;
if (!is_array($where)) {
$where = ['mmtid' => $where];
}
$special = array_intersect_key($parameters, ['name' => 1, 'hidden' => 1, 'weight' => 1]);
$not_special = array_diff_key($parameters, $special);
if ($special) {
if ($not_special) {
mm_content_update_quick($special, $where, $parent, FALSE);
$parameters = $not_special;
unset($special);
}
else {
$parameters['sort_idx_dirty'] = 1;
}
}
$schema = \Drupal::service('entity_field.manager')->getActiveFieldStorageDefinitions('mm_tree');
foreach (array_keys($parameters) as $p) {
if (!isset($schema[$p])) {
\Drupal::logger('mm')->critical('Unknown attribute %name in $parameters to mm_content_update_quick', ['%name' => $p]);
\Drupal::messenger()->addStatus(t('An error occurred.'));
return;
}
}
foreach (array_keys($where) as $p) {
if (!isset($schema[$p])) {
\Drupal::logger('mm')->critical('Unknown attribute %name in $where to mm_content_update_quick', ['%name' => $p]);
\Drupal::messenger()->addStatus(t('An error occurred.'));
return;
}
}
$db = Database::getConnection();
$update = $db->update('mm_tree');
$special_conditions = $db->condition('OR');
$fields = [];
foreach (array_intersect(array_keys($schema), array_keys($parameters)) as $field) {
if ($field != 'mmtid') {
$fields[$field] = $parameters[$field];
if (!empty($special) && $field != 'sort_idx_dirty') {
$special_conditions->condition($field, $parameters[$field], '<>');
$have_special = TRUE;
}
}
}
$update->fields($fields);
if (!empty($have_special)) {
$update->condition($special_conditions);
}
foreach ($where as $key => $val) {
if (is_array($val)) {
$update->condition($key, $val, 'IN');
}
else {
$update->condition($key, $val);
}
}
if ($update->execute()) { // There were affected rows
if ($revision) {
if (!isset($where['mmtid'])) {
unset($fields['sort_idx_dirty']);
if ($tree = mm_content_get($fields, [], 1)) {
$where['mmtid'] = $tree[0]->mmtid;
}
}
if (isset($where['mmtid'])) {
mm_content_write_revision($where['mmtid']);
}
}
if (!empty($special)) {
if (empty($parent)) {
$parent = NULL;
}
$child = !isset($where['mmtid']) ? NULL : $where['mmtid'];
if (!is_null($parent) || !is_null($child)) {
mm_content_update_sort_queue($parent, $child);
}
}
if (isset($parameters['alias'])) {
mm_content_clear_routing_cache_tagged($where['mmtid'] ?? []);
}
if (isset($where['mmtid'])) {
if (!is_null($parent)) {
$parameters['parent'] = $parent;
}
mm_content_notify_change('update_page_quick', $where['mmtid'], NULL, $parameters);
}
}
}
/**
* Makes a copy of an mm_tree entry to the mm_tree_revision table
*
* @param int $mmtid
* The tree ID of the entry being copied
*/
function mm_content_write_revision($mmtid) {
$is_default_revision = TRUE;
$has_metadata_key = FALSE;
if ($mm_tree = MMTree::load($mmtid)) {
$has_metadata_key = $mm_tree->getEntityType()->getRevisionMetadataKey('revision_default');
$is_default_revision = $mm_tree->isDefaultRevision();
}
$schema_rev = mm_get_table_columns('mm_tree_revision');
$db = Database::getConnection();
$select = $db->select('mm_tree', 't');
foreach ($schema_rev as $field) {
if (!in_array($field, ['vid', 'muid', 'mtime', 'revision_default'])) {
$select->addField('t', $field);
}
}
$select->addExpression(':muid', 'muid', [':muid' => \Drupal::currentUser()->id()]);
$select->addExpression(':mtime', 'mtime', [':mtime' => mm_request_time()]);
if ($has_metadata_key) {
$select->addExpression(':revision_default', 'revision_default', [':revision_default' => $is_default_revision]);
}
$select->condition('t.mmtid', $mmtid);
$vid = $db->insert('mm_tree_revision')
->from($select)
->execute();
if ($vid) {
$db->update('mm_tree')
->fields(['vid' => $vid])
->condition('mmtid', $mmtid)
->execute();
mm_content_invalidate_mm_tree_cache($mmtid);
}
}
/**
* Invalidate Drupal's cache for anything tagged with mm_tree:mmtid.
*
* @param int|MMTree $mm_tree_or_mmtid
* MMTree entity (preferred) or MMTID
*/
function mm_content_invalidate_mm_tree_cache($mm_tree_or_mmtid) {
if (!is_object($mm_tree_or_mmtid)) {
$mm_tree_or_mmtid = MMTree::load($mm_tree_or_mmtid);
if (!$mm_tree_or_mmtid) {
return;
}
}
Cache::invalidateTags($mm_tree_or_mmtid->getCacheTagsToInvalidate());
}
/**
* Based on an entry's permissions, return appropriate default values for 'who
* can edit or delete this content' on nodes added to it.
*
* @param int $mmtid
* The tree ID of the entry nodes are being added to
* @param array $grouplist
* On return, contains an array of long group names, indexed by the mmtid of
* the group
* @param array $userlist
* On return, contains an array of long usernames, indexed by uid
* @param int $max
* The maximum number of users to copy in ad-hoc groups
*/
function mm_content_get_default_node_perms($mmtid, &$grouplist, &$userlist, $max) {
// Find groups
$grouplist = [];
$db = Database::getConnection();
$select = $db->select('mm_tree', 't');
$select->join('mm_tree_access', 'a', 'a.mmtid = t.mmtid');
$select->leftJoin('mm_tree', 't2', 'a.gid = t2.mmtid');
$or = $db->condition('OR');
$select->fields('t2', ['mmtid', 'name'])
->distinct()
->condition('t2.mmtid', 0, '>=')
->condition($or
->condition('a.mode', Constants::MM_PERMS_APPLY)
->condition('a.mode', Constants::MM_PERMS_WRITE)
)
->condition('a.mmtid', $mmtid)
->orderBy('t2.name');
$result = $select->execute();
foreach ($result as $r) {
$grouplist[$r->mmtid] = $r->name;
}
// Find individual users
$userlist = [];
$or = $db->condition('OR');
$result = $db->select('mm_tree_access', 'a')
->fields('a', ['gid'])
->distinct()
->condition('a.gid', 0, '<')
->condition($or
->condition('a.mode', Constants::MM_PERMS_APPLY)
->condition('a.mode', Constants::MM_PERMS_WRITE)
)
->condition('a.mmtid', $mmtid)
->execute();
foreach ($result as $r) {
$users = mm_content_get_users_in_group($r->gid, NULL, TRUE, $max);
if (!is_null($users))
$userlist += $users;
}
}
/**
* Set a group's membership list or virtual group attributes
*
* @param int $mmtid
* MM tree ID of the group
* @param int|null $vgid
* Virtual group ID of the group, if known. If this value is NULL and the
* group is determined to be a virtual group, its vgid is retrieved
* @param string $qfield
* For a virtual group, the "column to select" portion of the query; ignored
* for non-virtual groups
* @param string $qfrom
* For a virtual group, the "FROM clause" portion of the query; ignored for
* non-virtual groups
* @param array|string $members
* For non-virtual groups, an array of the uids of the group's members; if an
* empty string (''), do not change any existing users
* @param string $large_group_form_token
* If large group management is used, contains the token associated with the
* input form.
* @param Connection $database
* (optional) The database connection to use.
*/
function mm_content_set_group_members($mmtid, $vgid, $qfield, $qfrom, $members, $large_group_form_token = '', Connection $database = NULL) {
$database = $database ?: Database::getConnection();
$vgroup = mm_content_is_vgroup($mmtid);
if ($vgroup && empty($vgid)) {
$select = $database->select('mm_group', 'g');
$select->join('mm_vgroup_query', 'v', 'g.vgid = v.vgid');
$select->fields('v', ['vgid'])
->condition('g.gid', $mmtid);
$vgid = $select->execute()->fetchField();
}
if ($vgroup && !empty($qfield) && !empty($vgid)) {
$database->update('mm_vgroup_query')
->fields(['field' => $qfield, 'qfrom' => $qfrom, 'dirty' => Constants::MM_VGROUP_DIRTY_NEXT_CRON])
->condition('vgid', $vgid)
->execute();
}
elseif ($vgroup || is_array($members) || !empty($large_group_form_token)) {
// DELETE FROM {mm_vgroup_query} WHERE
// (SELECT 1 FROM {mm_group} g WHERE g.vgid = {mm_vgroup_query}.vgid
// AND g.gid = :mmtid)
$mm_group = $database->select('mm_group', 'g');
$mm_group->addExpression(1);
$mm_group->where('g.vgid = {mm_vgroup_query}.vgid')
->condition('g.gid', $mmtid);
$database->delete('mm_vgroup_query')
->condition($mm_group)
->execute();
$database->delete('mm_group')
->condition('gid', $mmtid)
->execute();
if (!empty($large_group_form_token)) { // Copy from the temp table and then remove temp records
$select = $database->select('mm_group_temp', 'g');
$select->addExpression($mmtid, 'gid');
$select->addField('g', 'uid');
$select->addExpression('0', 'vgid');
$select->condition('sessionid', session_id());
$select->condition('token', $large_group_form_token);
$database->insert('mm_group')
->from($select)
->execute();
$database->delete('mm_group_temp')
->condition('sessionid', session_id())
->condition('token', $large_group_form_token)
->execute();
}
if ($vgroup) {
if (!empty($qfield)) {
$vgid = $database->insert('mm_vgroup_query')
->fields(['field' => $qfield, 'qfrom' => $qfrom, 'dirty' => Constants::MM_VGROUP_DIRTY_NEXT_CRON])
->execute();
$database->insert('mm_group')
->fields(['gid' => $mmtid, 'uid' => 0, 'vgid' => $vgid])
->execute();
}
}
elseif (is_array($members)) {
foreach ($members as $uid) {
$database->insert('mm_group')
->fields(['gid' => $mmtid, 'uid' => $uid, 'vgid' => 0])
->execute();
}
}
}
}
/**
* Given a node, return TRUE if the node is in a recycle bin.
*
* @param NodeInterface $node
* The node to test
* @param int|string $mmtid
* Either the MM Tree ID of the page (recycle bin) to test against, or one of
* these constants:
* - MM_NODE_RECYCLED_MMTID_CURR: Recycled on the current page (bin)
* - MM_NODE_RECYCLED_MMTID_EXCL: Recycled on all pages where it is shown
* @return bool
* TRUE if the node is in a recycle bin
*/
function mm_content_node_is_recycled(NodeInterface $node, $mmtid = Constants::MM_NODE_RECYCLED_MMTID_EXCL) {
if (!isset($mmtid) || empty($node->__get('recycle_date'))) {
return FALSE;
}
if ($mmtid == Constants::MM_NODE_RECYCLED_MMTID_CURR) {
mm_parse_args($term_ids, $oarg_list, $mmtid);
if (is_null($mmtid)) {
return FALSE;
}
}
$db = Database::getConnection();
if ($mmtid == Constants::MM_NODE_RECYCLED_MMTID_EXCL) {
return (bool) $db->query('SELECT COUNT(*) = 0 FROM {mm_node2tree} n2 LEFT JOIN {mm_recycle} r ON n2.nid = r.id AND `type` = :type AND (r.from_mmtid = n2.mmtid OR r.bin_mmtid = n2.mmtid) WHERE n2.nid = :nid AND r.id IS NULL',
[':type' => 'node', ':nid' => $node->id()])->fetchField();
}
return (bool) $db->query('SELECT COUNT(*) FROM {mm_recycle} WHERE `type` = :type AND id = :nid AND (bin_mmtid = :mmtid OR from_mmtid = :mmtid)',
[':type' => 'node', ':nid' => $node->id(), ':mmtid' => $mmtid])->fetchField();
}
/**
* Given a set of tree IDs, return a list of the node IDs assigned to them.
*
* @param int|array $mmtids
* An array of tree IDs, or a single ID
* @param int|string $limit
* Optional limit to the number of results
* @param bool $unique
* If set, return only those nodes that are assigned to entries from the list,
* and not to other entries.
* @return array
* An array of node IDs
*/
function mm_content_get_nids_by_mmtid($mmtids, $limit = '', $unique = FALSE) {
if (!is_array($mmtids)) {
$mmtids = [$mmtids];
}
if (count($mmtids) == 1) {
$in = '= :one';
$args = [':one' => $mmtids[0]];
}
else {
$in = 'IN(:list[])';
$args = [':list[]' => $mmtids];
}
if ($unique) {
$query = 'SELECT t1.nid FROM {mm_node2tree} t1 ' .
"INNER JOIN {mm_node2tree} t2 ON t1.nid = t2.nid AND t2.mmtid $in " .
"GROUP BY t1.nid HAVING SUM(IF(t1.mmtid $in, 1, 0)) = COUNT(*)";
}
else {
$query = "SELECT nid FROM {mm_node2tree} WHERE mmtid $in";
}
$db = Database::getConnection();
if ($limit) {
return $db->queryRange($query, 0, $limit, $args)->fetchCol();
}
return $db->query($query, $args)->fetchCol();
}
/**
* Given a tree ID, return a SQL query handle for nodes that appear on it. Only
* nodes the user has permission to view are returned, and they are sorted
* according to all the various possible criteria.
*
* @param int $mmtid
* The tree ID of the entry
* @param int $per_page
* Nodes per page in the pager (optional)
* @param int $element
* Pager element number (optional)
* @param string $add_select
* Text to add to the SELECT part of the outer query (optional)
* @param string $add_join
* Additional LEFT JOINs for the outer query (optional)
* @param string $add_inner_where
* Text to add to the WHERE part of the inner query (optional)
* @param string $add_outer_where
* Text to add to the WHERE part of the outer query (optional)
* @param string $add_groupby
* Text to add to the GROUP BY part of the outer query (optional)
* @param string $add_orderby
* Text to add to the ORDER BY part of the outer query (optional)
* @return StatementInterface
* An SQL query handle
*/
function mm_content_get_accessible_nodes_by_mmtid($mmtid, $per_page = 0, $element = 0, $add_select = '', $add_join = '', $add_inner_where = '', $add_outer_where = '', $add_groupby = '', $add_orderby = '') {
if ($mmtid < 0) {
return NULL;
}
$q = mm_content_get_accessible_nodes_by_mmtid_query($mmtid, $count_sql, $add_select, $add_join, $add_inner_where, $add_outer_where, $add_groupby, $add_orderby);
$db = Database::getConnection();
if ($per_page == 0 && Constants::MM_MAX_NUMBER_OF_NODES_PER_PAGE != 0) {
return $db->queryRange($q, 0, Constants::MM_MAX_NUMBER_OF_NODES_PER_PAGE);
}
if ($per_page > 0) {
$total = $db->query($count_sql)->fetchField();
/** @var Pager $pager */
$pager = \Drupal::service('pager.manager')->createPager($total, $per_page, $element);
return $db->queryRange($q, $per_page * $pager->getCurrentPage(), $per_page);
}
if ($per_page == -2) {
$start_value = \Drupal::request()->query->getInt('page', 0) * Constants::MM_LAZY_LOAD_NUMBER_OF_NODES;
return $db->queryRange($q, $start_value, Constants::MM_LAZY_LOAD_NUMBER_OF_NODES);
}
return $db->query($q);
}
/**
* Given a tree ID, return a SQL query for nodes that appear on it. Only nodes
* that are owned by the user or are currently published are returned, and they
* are sorted according to all the various possible criteria.
*
* @param int|array $mmtid
* The tree ID (or array of tree IDs) of the entry
* @param string &$count_sql
* The SQL query for querying the count of matches
* @param string $add_select
* Text to add to the SELECT part of the outer query (optional)
* @param string $add_join
* Additional LEFT JOINs for the outer query (optional)
* @param string $add_inner_where
* Text to add to the WHERE part of the inner query (optional)
* @param string $add_outer_where
* Text to add to the WHERE part of the outer query (optional)
* @param string $add_groupby
* Text to add to the GROUP BY part of the outer query (optional)
* @param string $add_orderby
* Text to add to the ORDER BY part of the outer query (optional)
* @return string
* The SQL query
*/
function mm_content_get_accessible_nodes_by_mmtid_query($mmtid, &$count_sql, $add_select = '', $add_join = '', $add_inner_where = '', $add_outer_where = '', $add_groupby = '', $add_orderby = '') {
$mmtid = is_array($mmtid) ? 'IN(' . join(',', $mmtid) . ')' : "= $mmtid";
// This includes the hack "n.changed AS created" to allow node_feed() to show
// the changed date instead of the post date. It is called in _mm_render_pages().
// Woe unto he who attempts to grok this.
$now = mm_request_time();
$scheduled = "IFNULL((s.publish_on = 0 OR s.publish_on <= $now) AND (s.unpublish_on = 0 OR $now < s.unpublish_on), 1)";
$inner =
'FROM {mm_tree} tr ' .
'INNER JOIN {mm_node2tree} t ON t.mmtid = tr.mmtid ' .
'INNER JOIN {node} n ON n.nid = t.nid ' .
'INNER JOIN {node_field_data} nfd ON nfd.nid = t.nid ' .
'LEFT JOIN {mm_node_schedule} s ON s.nid = n.nid ' .
'LEFT JOIN {mm_node_reorder} r ON r.nid = n.nid AND r.mmtid = tr.mmtid ';
$where = " WHERE tr.mmtid $mmtid";
// skip some tests for users with 'bypass node access' permission
if (!\Drupal::currentUser()->hasPermission('bypass node access')) {
$current_uid = \Drupal::currentUser()->id();
$node_writable = $current_uid > 0 ? " OR (nfd.uid = $current_uid OR gw.uid = $current_uid OR vw.uid = $current_uid)" : '';
$inner .=
'LEFT JOIN {mm_node_write} nw ON nw.nid = n.nid ' .
'LEFT JOIN {mm_group} gw ON gw.gid = nw.gid ' .
'LEFT JOIN {mm_virtual_group} vw ON vw.vgid = gw.vgid';
$where .= " AND (nfd.status = 1 AND $scheduled$node_writable)$add_inner_where ";
}
elseif ($add_inner_where) {
$where .= $add_inner_where;
}
$count_inner = $inner . $add_join . $where;
$inner .= $where;
if ($add_outer_where) {
$count_inner .= $add_outer_where;
$add_outer_where = ' WHERE' . preg_replace('/^\s*(\S+)/', '', $add_outer_where); // Remove AND, OR, etc.
}
$count_sql = 'SELECT COUNT(DISTINCT n.nid) ' . $count_inner;
return
'SELECT n.nid, ' .
'IF(MAX(n.set_change_date) = 1 AND MAX(n.publish_on) > 0, MAX(n.publish_on), MAX(n.changed)) AS created, ' . // creation date
'MAX(n.sticky) AND (MAX(n.uid) = 1 OR MAX(n.owns_it) OR ' .
'COUNT(v.uid = n.uid OR g.vgid = 0 AND g.uid = n.uid)) AS stuck, ' . // node is sticky
"MAX(n.scheduled) AS scheduled, MAX(n.status) AS status, MAX(n.title) AS title$add_select " .
'FROM ' .
'(SELECT DISTINCT n.*, nfd.changed, nfd.uid, nfd.sticky, nfd.status, nfd.title, t.mmtid, r.weight, r.region, s.set_change_date, s.publish_on, ' .
"$scheduled AS scheduled, " .
'(nfd.uid = tr.uid) AS owns_it ' . // entry is owned by node's owner
$inner .
') AS n ' .
"$add_join " .
"LEFT JOIN {mm_tree_access} a ON a.mode = '" . Constants::MM_PERMS_WRITE . "' AND a.mmtid = n.mmtid " .
'LEFT JOIN {mm_group} g ON a.gid = g.gid ' .
'LEFT JOIN {mm_virtual_group} v ON g.vgid = v.vgid ' .
'AND (v.uid = n.uid OR g.vgid = 0 AND g.uid = n.uid) ' .
$add_outer_where .
"GROUP BY n.nid$add_groupby " .
"ORDER BY stuck DESC, MAX(n.weight), created DESC, title$add_orderby";
}
/**
* Return whether the user has some type of access on a given Drupal node. This
* takes into account both the permissions on the node itself (for writing) and
* the permissions of the tree entry it's in (for everything else.)
*
* @param NodeInterface $node
* The node object on which the operation is to be performed.
* @param string $op
* The operation to be performed on the node. Possible values are:
* - "view"
* - "update"
* - "delete"
* @param AccountInterface $account
* The user object on which the operation is to be performed. (optional)
* @return bool|void
* TRUE if the operation may be performed.
*/
function mm_content_node_access(NodeInterface $node, $op, AccountInterface $account = NULL) {
static $recursive;
switch ($op) {
case 'view':
foreach (mm_module_implements('mm_node_access') as $function) {
$out = call_user_func_array($function, [$op, $node, $account]);
if (isset($out)) {
return $out;
}
}
return mm_content_user_can_node($node, Constants::MM_PERMS_READ, $account);
case 'update':
case 'delete':
if (!$account) {
$account = \Drupal::currentUser();
}
if (!$account->isAuthenticated()) {
return FALSE;
}
if ($account->id() == 1) {
// admin user
return TRUE;
}
if ($account->hasPermission('bypass node access')) {
return TRUE;
}
foreach (mm_module_implements('mm_node_access') as $function) {
$out = call_user_func_array($function, [$op, $node, $account]);
if (isset($out)) {
return $out;
}
}
if ($node->getOwnerId() === $account->id()) {
// regular owner
return TRUE;
}
// being called recursively from $test_node->access() call, below
if ($recursive) {
return;
}
if (mm_content_user_can_update_node($node, $account)) {
$test_node = isset($node->type) ? clone($node) : Node::load($node->id());
$test_node->setOwnerId($account->id());
$recursive = TRUE;
$ret = $test_node->access($op, $account);
$recursive = FALSE;
return $ret;
}
}
return FALSE;
}
/**
* Return whether the user can update (write to) a node, based solely on the MM
* permissions of that node. Page-level permissions are not considered.
*
* @param NodeInterface $node
* The node object being queried.
* @param AccountInterface $account
* The user to test. (optional)
* @return bool
* TRUE if the user can update.
*/
function mm_content_user_can_update_node(NodeInterface $node, AccountInterface $account = NULL) {
if (!$account) {
$account = \Drupal::currentUser();
}
if (mm_site_is_disabled($account)) {
return FALSE;
}
$db = Database::getConnection();
$select = $db->select('mm_node_write', 'nw');
$select->leftJoin('mm_group', 'g', 'nw.gid = g.gid');
$select->leftJoin('mm_virtual_group', 'v', 'v.vgid = g.vgid');
$select->fields('nw', ['gid'])
->distinct()
->condition('nw.nid', $node->id());
$or = $db->condition('OR');
// everyone, or this user is in a matching group
if ($account->isAuthenticated()) {
$select->condition($or
->condition('nw.gid', 0)
->condition('g.uid', $account->id())
->condition('v.uid', $account->id())
);
}
else {
$select->condition($or
->condition('nw.gid', 0)
->condition('g.uid', 0)
->condition('v.uid', 0)
);
}
return mm_retry_query($select->countQuery())->fetchField() > 0;
}
/**
* Flag a virtual group as needing its data regenerated. The actual update of
* mm_virtual_group happens during monster_menus_cron().
*
* @param int $mmtid
* Tree/Group ID (not virtual group ID!) of the virtual group to be updated.
* Omit or set to NULL to mark all groups for update.
*/
function mm_content_update_vgroup_view($mmtid = NULL) {
$db = Database::getConnection();
if (!isset($mmtid)) {
$db->update('mm_vgroup_query')
->fields(['dirty' => Constants::MM_VGROUP_DIRTY_NEXT_CRON])
->condition('dirty', Constants::MM_VGROUP_DIRTY_NOT)
->execute();
}
else {
// UPDATE {mm_vgroup_query} q INNER JOIN {mm_group} g ON g.vgid = q.vgid
// SET q.dirty = <MM_VGROUP_DIRTY_NEXT_CRON>
// WHERE g.gid = $mmtid AND dirty = <MM_VGROUP_DIRTY_NOT>
$group = $db->select('mm_group', 'g');
$group->condition('g.gid', $mmtid)
->where('g.vgid = {mm_vgroup_query}.vgid');
$group
->addExpression('COUNT(*)');
$db->update('mm_vgroup_query')
->condition($group, '0', '<>')
->condition('dirty', Constants::MM_VGROUP_DIRTY_NOT)
->fields(['dirty' => Constants::MM_VGROUP_DIRTY_NEXT_CRON])
->execute();
}
}
/**
* Reset the sort order of nodes on a page without affecting their region
* assignments.
*
* @param int $mmtid
* The Tree ID of the containing page.
* @param int $nid
* Optional ID of a particular node to alter. If unset, all nodes are altered.
*/
function mm_content_reset_custom_node_order($mmtid, $nid = NULL) {
// Remove entries in the "content" region.
$db = Database::getConnection();
$q = $db->delete('mm_node_reorder')
->condition('mmtid', $mmtid)
->condition('region', NULL);
if (isset($nid)) {
$q->condition('nid', $nid);
}
$q->execute();
// Reset order to 0 in all other places.
$q = $db->update('mm_node_reorder')
->condition('mmtid', $mmtid)
->fields(['weight' => 0]);
if (isset($nid)) {
$q->condition('nid', $nid);
}
$q->execute();
}
function _mm_content_active_regions() {
$out = [];
foreach (\Drupal::service('theme_handler')->listInfo() as $data) {
if ($data->status) {
foreach (array_keys($data->info['regions']) as $region) {
if (!isset($data->info['regions_hidden']) || !in_array($region, $data->info['regions_hidden'])) {
$out[] = $region;
}
}
}
}
return array_unique($out);
}
/**
* Get a list of allowed content types in one or all regions.
*
* @param string $region
* If set, return just the types for this one region. Otherwise, return an
* array indexed by region.
* @return array
* An array, as described above.
*/
function mm_content_get_perms_for_region($region = NULL) {
if (is_null($region)) {
$out = [];
foreach (_mm_content_active_regions() as $region) {
$out[$region] = mm_content_get_perms_for_region($region);
}
return $out;
}
$list = mm_get_setting('nodes.allowed_region_perms');
if (isset($list['users'][$region])) {
return [
'everyone' => $list['everyone'][$region],
'users' => $list['users'][$region],
'groups' => $list['groups'][$region],
];
}
return ['everyone' => $region == Constants::MM_UI_REGION_CONTENT, 'users' => [], 'groups' => []];
}
/**
* Get a list of allowed content types in one or all regions.
*
* @param string $region
* If set, return just the types for this one region. Otherwise, return an
* array indexed by region.
* @return array|string
* An array, as described above.
*/
function mm_content_get_allowed_types_for_region($region = NULL) {
if (is_null($region)) {
$out = [];
foreach (_mm_content_active_regions() as $region) {
$out[$region] = mm_content_get_allowed_types_for_region($region);
}
return $out;
}
$types = mm_get_setting('nodes.allowed_region_node_types');
return $types[$region] ?? 'all';
}
/**
* Get a list of regions into which a particular user can place nodes.
*
* @param AccountInterface $account
* The user object to test, or NULL to use the current user.
* @param string $type
* If set, the list is further limited to only those regions that are allowed
* for a specific content type.
* @return array
* An array containing the names of all allowed regions.
*/
function mm_content_get_allowed_regions_for_user(AccountInterface $account = NULL, $type = NULL) {
static $allowed = [];
if (!isset($account)) {
$account = \Drupal::currentUser();
}
$uid = $account->id();
$bypass = $uid == 1 || $account->hasPermission('administer all menus');
if (!isset($allowed[$uid])) {
foreach (_mm_content_active_regions() as $region) {
$ok = FALSE;
if ($uid) {
if ($bypass) {
$ok = TRUE;
}
else {
$perms = mm_content_get_perms_for_region($region);
if (!empty($perms['everyone']) || isset($perms['users']) && in_array($uid, $perms['users'])) {
$ok = TRUE;
}
else {
if (!isset($user_groups)) {
$user_groups = mm_content_get_uids_in_group(NULL, $uid, TRUE, TRUE, FALSE);
}
if (isset($perms['groups']) && array_intersect($perms['groups'], $user_groups)) {
$ok = TRUE;
}
}
}
}
if ($ok) {
$allowed[$uid][] = $region;
}
}
// Allow third-party modules to alter the allowed types for this user.
\Drupal::moduleHandler()->alter('mm_allowed_regions_for_user', $allowed[$uid], $account, $type);
}
if ($type && !$bypass) {
$list = $allowed[$uid];
foreach ($list as $i => $region) {
$types_for_region = mm_content_get_allowed_types_for_region($region);
if ($types_for_region !== 'all' && !in_array($type, $types_for_region)) {
unset($list[$i]);
}
}
return $list;
}
return $allowed[$uid];
}
/**
* Figure out if a given node is assigned to one or more hidden tree entries
*
* @param NodeInterface|int $nid
* The node object or node number being queried
* @param bool $all_pages
* If TRUE, return TRUE when all pages on which the node appears are hidden.
* Otherwise, return TRUE if any page is hidden.
* @return bool
* TRUE if the node is assigned to one or more hidden tree entries
*/
function mm_content_is_hidden_node($nid, $all_pages = TRUE) {
if (is_object($nid)) {
$nid = $nid->id();
}
$db = Database::getConnection();
if ($all_pages) {
$inner = $db->select('mm_node2tree', 'n');
$inner->leftJoin('mm_tree_parents', 'p', 'p.mmtid = n.mmtid');
$inner->leftJoin('mm_tree', 't', 't.mmtid = p.parent OR t.mmtid = n.mmtid');
$inner->addExpression('SUM(t.hidden) > 0', 'hidden');
$inner->condition('n.nid', $nid);
$inner->groupBy('p.mmtid');
$select = $db->select($inner, 'x');
$select->addExpression('COUNT(*) = SUM(x.hidden)', 'is_hidden');
return $select->execute()->fetchField() > 0;
}
$select = $db->select('mm_node2tree', 'n');
$select->leftJoin('mm_tree_parents', 'p', 'p.mmtid = n.mmtid');
$select->leftJoin('mm_tree', 't', 't.mmtid = p.parent OR t.mmtid = n.mmtid');
return $select->condition('n.nid', $nid)
->condition('t.hidden', 1)
->countQuery()->execute()->fetchField() > 0;
}
/**
* Delete a recycling bin tree node, if it's empty
*
* @param int $bin
* Tree ID of the bin to possibly delete
* @return string|bool
* An error message, if an error occurs; otherwise, TRUE if the bin was
* deleted.
*/
function mm_content_delete_bin($bin) {
if (($tree = mm_content_get($bin)) && $tree->name == Constants::MM_ENTRY_NAME_RECYCLE) {
// It's definitely a bin.
$db = Database::getConnection();
if (!$db->query('SELECT COUNT(*) FROM {mm_node2tree} WHERE mmtid = :mmtid', [':mmtid' => $bin])->fetchField()) {
// No nodes in the bin itself.
if ($db->query('SELECT COUNT(*) FROM {mm_tree} WHERE parent = :mmtid', [':mmtid' => $bin])->fetchField() == 0) {
// It's empty.
$err = mm_content_delete($bin, FALSE);
return $err ?: TRUE;
}
}
}
return FALSE;
}
/**
* Move content to the recycle bin, creating the bin if needed
*
* @param int|array $mmtids
* Array, or a single tree ID to move
* @param int|array $nids
* Array, or a single node ID to move. Either an ordered array or an
* associative array can be used. If an ordered array is used, then it
* contains a list of node IDs which will be moved into a recycle bin for each
* page to which they are assigned. If an associative array is used, then each
* key must be the node ID and each value must be an array of Tree IDs from
* which the node should be removed.
* @return string|int|void
* Either an error message or the MM Tree ID of the bin
*/
function mm_content_move_to_bin($mmtids = NULL, $nids = NULL) {
$bin = NULL;
$db = Database::getConnection();
if (isset($mmtids)) { // recycle one or more entries
if (!is_array($mmtids)) {
$mmtids = [$mmtids];
}
$txn = $db->startTransaction();
foreach ($mmtids as $mmtid) {
$bin_parent = mm_content_get_parent($mmtid);
if (!$bin_parent) {
return;
}
$bin = _mm_content_make_recycle($bin_parent);
if (!is_numeric($bin)) {
return $bin;
}
$bin = intval($bin);
if (!($tree = mm_content_get($mmtid))) {
mm_content_delete_bin($bin);
return 'error';
}
$n = 0;
$name = $tree->name;
$alias = $tree->alias;
while ($db->query("SELECT COUNT(*) FROM {mm_tree} WHERE (name = :name OR alias <> '' AND alias = :alias) AND parent = :parent", [':name' => $name, ':alias' => $alias, ':parent' => $bin])->fetchField()) {
$n++;
$name = $tree->name . " ($n)";
$alias = empty($tree->alias) ? '' : $tree->alias . "-$n";
}
if ($n) {
mm_content_update_quick(['name' => $name, 'alias' => $alias], ['mmtid' => $mmtid], $tree->parent);
}
// Make sure the sort index is up to date, then force a re-read of the
// source tree entry.
mm_content_update_sort_queue();
mm_content_clear_caches($mmtid);
$err = mm_content_move($mmtid, $bin, 'recycle');
if ($err) {
mm_content_delete_bin($bin);
return $err;
}
$db->insert('mm_recycle')
->fields(['type' => 'cat', 'id' => $mmtid, 'bin_mmtid' => $bin, 'recycle_date' => mm_request_time()])
->execute();
}
}
elseif (isset($nids)) { // recycle one or more nodes
if (!is_array($nids)) {
$nids = [$nids];
}
else if (!$nids) {
return;
}
// Convert to an associative array, if needed.
$assoc = [];
$nid_list = isset($nids[0]) ? $nids : array_keys($nids);
foreach ($nid_list as $nid) {
$from_mmtids = mm_content_get_by_nid($nid);
// If the node is not on any page, create the bin at the root.
if (!$from_mmtids) {
$from_mmtids = [0];
}
else {
// Exclude recycle bins
foreach ($from_mmtids as $key => $from_mmtid) {
if (mm_content_is_recycle_bin($from_mmtid)) {
unset($from_mmtids[$key]);
}
}
}
// If it's already associative, make sure the supplied mmtid list is
// valid.
if (!isset($nids[0])) {
$from_mmtids = array_intersect($from_mmtids, $nids[$nid]);
}
// Convert to associative nid => [array of mmtids] for each nid
if ($from_mmtids) {
$assoc[$nid] = $from_mmtids;
}
}
$txn = $db->startTransaction();
foreach ($assoc as $nid => $from_mmtids) {
foreach ($from_mmtids as $mmtid) {
$bin = _mm_content_make_recycle($mmtid);
if (!is_numeric($bin)) {
return $bin;
}
$bin = intval($bin);
// Remove from old page.
$db->delete('mm_node2tree')
->condition('nid', $nid)
->condition('mmtid', $mmtid)
->execute();
// Clear any custom reordering.
$db->delete('mm_node_reorder')
->condition('nid', $nid)
->condition('mmtid', $mmtid)
->execute();
// Store recovery state in mm_recycle.
$db->merge('mm_recycle')
->keys(['type' => 'node', 'id' => $nid, 'from_mmtid' => $mmtid])
->fields(['bin_mmtid' => $bin, 'recycle_date' => mm_request_time()])
->execute();
// Add to bin.
$db->insert('mm_node2tree')
->fields(['nid' => $nid, 'mmtid' => $bin])
->execute();
\Drupal::logger('mm')->notice('Recycled node=@nid from mmtid=@mmtid to bin=@bin', ['@nid' => $nid, '@mmtid' => $mmtid, '@bin' => $bin]);
}
}
// Clear the cache used by mm_content_get_by_nid.
mm_content_get_by_nid(NULL, TRUE);
mm_content_clear_node_cache($nid_list);
// Commit.
unset($txn);
}
return $bin;
}
/**
* Move content out of the recycle bin
*
* @param array|int|null $mmtids
* Array, or a single tree ID to move out
* @param array|NodeInterface|null $nodes
* Array, or a single node object to move out.
* @param int $node_bin_mmtid
* If supplied, the nodes are restored from only this bin. Otherwise, they are
* restored from all bins at once; this is usually not the desired result.
* @param bool $use_watchdog
* If TRUE, log a message to the watchdog upon successful restoration.
* @return string|void
* An error message, if an error occurred
*/
function mm_content_move_from_bin($mmtids, $nodes = NULL, $node_bin_mmtid = NULL, $use_watchdog = TRUE) {
$db = Database::getConnection();
if (isset($mmtids)) {
if (!is_array($mmtids)) {
$mmtids = [$mmtids];
}
$txn = $db->startTransaction();
foreach ($mmtids as $mmtid) {
$bin_parent = mm_content_get_parent($bin = mm_content_get_parent($mmtid));
$error = mm_content_move($mmtid, $bin_parent, 'restore');
if (is_string($error)) {
return $error;
}
$db->delete('mm_recycle')
->condition('type', 'cat')
->condition('id', $mmtid)
->execute();
$error = mm_content_delete_bin($bin);
if (is_string($error)) {
return $error;
}
}
}
else if (isset($nodes)) {
if (!is_array($nodes)) {
$nodes = [$nodes];
}
$txn = $db->startTransaction();
$clear_nids = $clear_mmtids = [];
foreach ($nodes as $node) {
$nid = $clear_nids[] = $node->id();
if (!empty($node_bin_mmtid)) {
$bins = [$node_bin_mmtid];
$camefrom = $db->query("SELECT from_mmtid FROM {mm_recycle} WHERE type = 'node' AND id = :nid AND bin_mmtid = :bin_mmtid",
[':nid' => $nid, ':bin_mmtid' => $node_bin_mmtid])->fetchCol();
}
else {
$bins = $node->__get('recycle_bins');
$camefrom = $node->__get('recycle_from_mmtids');
}
$db->delete('mm_recycle')
->condition('type', 'node')
->condition('id', $nid)
->condition('bin_mmtid', $bins, 'IN')
->execute();
foreach ($camefrom as $mmtid) {
if ($mmtid) {
$db->insert('mm_node2tree')
->fields(['nid' => $nid, 'mmtid' => $mmtid])
->execute();
if ($use_watchdog) {
\Drupal::logger('mm')->notice('Restored node=@nid to @mmtid', ['@nid' => $nid, '@mmtid' => $mmtid]);
}
}
}
foreach ($bins as $bin) {
$db->delete('mm_node2tree')
->condition('nid', $nid)
->condition('mmtid', $bin)
->execute();
$err = mm_content_delete_bin($bin);
if (is_string($err)) {
return $err;
}
}
$clear_mmtids = array_merge($clear_mmtids, $camefrom);
}
mm_content_clear_node_cache($clear_nids);
mm_content_clear_page_cache($clear_mmtids);
// Clear the cache used by mm_content_get_by_nid.
mm_content_get_by_nid(NULL, TRUE);
}
}
/**
* Remove references to a node from a page. If the page is a recycle bin, it is
* deleted if it becomes empty.
*
* Note that this function does not ensure that the removed nodes will still
* appear on a page somewhere after the operation is complete. This is the
* responsibility of the caller.
*
* @param array|int $nids
* Array, or a single node ID to remove
* @param array|int $mmtids
* Array, or a single tree ID from which the node(s) should be removed
* @return string|void
* An error message, if there is an error
*/
function mm_content_remove_node_from_page($nids, $mmtids) {
if (!is_array($nids)) {
$nids = [$nids];
}
if (!is_array($mmtids)) {
$mmtids = [$mmtids];
}
$db = Database::getConnection();
$txn = $db->startTransaction();
foreach ($mmtids as $mmtid) {
$db->delete('mm_node2tree')
->condition('nid', $nids, 'IN')
->condition('mmtid', $mmtid)
->execute();
if (mm_content_is_recycle_bin($mmtid)) {
$bin = $mmtid;
}
else if (mm_content_is_recycled($mmtid)) {
foreach (array_reverse(mm_content_get_parents($mmtid)) as $bin) {
if (mm_content_is_recycle_bin($bin)) {
break;
}
}
}
if (isset($bin)) {
$db->delete('mm_recycle')
->condition('type', 'node')
->condition('id', $nids, 'IN')
->condition('bin_mmtid', $bin)
->execute();
if ($bin == $mmtid) {
$err = mm_content_delete_bin($mmtid);
if (is_string($err)) {
return $err;
}
}
}
}
// Clear the cache used by mm_content_get_by_nid.
mm_content_get_by_nid(NULL, TRUE);
}
function mm_content_recycle_enabled() {
return mm_get_setting('recycle_auto_empty') >= 0;
}
function mm_content_is_node_content_block($mmtid) {
static $blocks_with_node_contents;
if (!isset($blocks_with_node_contents)) {
$blocks_with_node_contents = array_keys(mm_get_mm_blocks(['settings.show_node_contents' => 1]));
}
if (!$blocks_with_node_contents) {
return FALSE;
}
$select = Database::getConnection()->select('mm_tree_block', 'tb');
$select->condition('tb.mmtid', $mmtid);
$select->condition('tb.bid', $blocks_with_node_contents, 'IN');
return $select->countQuery()->execute()->fetchField();
}
/**
* Implements hook_views_pre_render()
*
* Prevent users from seeing results in views they should not be able to. This
* is imperfect because it removes data without considering pagination, so
* result sets have an uneven number of results per page. It also does not
* consider appearance schedule, so nodes may appear when they should not. This
* code should be considered mostly as a failsafe, to prevent unwanted data
* disclosure based on permission.
*
* @inheritdoc
*/
function monster_menus_views_pre_render(ViewExecutable $view) {
$cache = [];
foreach ($view->result as $index => $result) {
if (!empty($result->nid)) {
if (!isset($cache[$result->nid])) {
$cache[$result->nid] = mm_content_user_can_node($result->_entity ?? $result->nid, Constants::MM_PERMS_READ);
}
if (!$cache[$result->nid]) {
unset($view->result[$index]);
}
}
}
}
/**
* Implements hook_mm_tree_flags()
*/
function monster_menus_mm_tree_flags() {
return [
'limit_alias' => ['#type' => 'checkbox', '#description' => t('Prevents non-admin users from changing the item\'s alias')],
'limit_move' => ['#type' => 'checkbox', '#description' => t('Prevents non-admin users from moving the item')],
'limit_delete' => ['#type' => 'checkbox', '#description' => t('Prevents non-admin users from deleting the item')],
'limit_hidden' => ['#type' => 'checkbox', '#description' => t('Prevents non-admin users from changing "Don\'t show this page in the menu"')],
'limit_location' => ['#type' => 'checkbox', '#description' => t('Prevents non-admin users from changing the item\'s location on screen')],
'limit_name' => ['#type' => 'checkbox', '#description' => t('Prevents non-admin users from changing the item\'s name')],
'limit_write' => ['#type' => 'checkbox', '#description' => t('Prevents non-admin users from changing "Delete or change settings"')],
'no_breadcrumb' => ['#type' => 'checkbox', '#description' => t('Prevents the page breadcrumb from showing at this level'), '#flag_inherit' => TRUE],
'no_index' => ['#type' => 'checkbox', '#description' => t('Adds a meta tag asking crawlers to not index the page'), '#flag_inherit' => TRUE],
'user_home' => ['#type' => 'textfield', '#description' => t('If a user homepage, contains the uid'), '#flag_copy' => FALSE],
];
}
/**
* Insert or update an entry's flags.
*
* @param int $mmtid
* The MM tree ID of the entry to change
* @param string|array $flags
* A single string or array of strings in flag => value format to be added to
* the entry
* @param bool $clear_old
* If TRUE, remove any existing flags. Otherwise, add the new flags to the
* existing set. If the calling function has only just created the entry, use
* FALSE to avoid the overhead of trying to delete flags that aren't even set.
* @param Connection $database
* (optional) The database connection to use.
*/
function mm_content_set_flags($mmtid, $flags, $clear_old = TRUE, Connection $database = NULL) {
$database = $database ?: Database::getConnection();
if ($clear_old) {
$database->delete('mm_tree_flags')
->condition('mmtid', $mmtid)
->execute();
mm_content_notify_change('clear_flags', $mmtid, NULL, $flags);
}
if (!empty($flags)) {
if (!is_array($flags)) {
$flags = [$flags => ''];
}
foreach ($flags as $flag => $data) {
$database->insert('mm_tree_flags')
->fields(['mmtid' => $mmtid, 'flag' => $flag, 'data' => $data])
->execute();
}
mm_content_notify_change('insert_flags', $mmtid, NULL, $flags);
}
}
/**
* Get the permissions of an item in the MM tree. NOTE: The data returned by
* this function should never be displayed directly in the UI, since there can
* be reasons for it to be hidden from the user. The function
* mm_content_get_users_in_group() takes this into account.
*
* @param int $mmtid
* The MM tree ID of the entry to query
* @param bool $users
* Return the permissions of individual users
* @param bool $groups
* Return the permission groups
* @param bool $group_names
* When returning groups, set the key to the gid, and the value to the name of
* the group. This option should be FALSE (default) if the data is to be
* passed to mm_content_set_perms() or mm_content_insert_or_update().
* @param Connection $database
* (optional) The database connection to use.
* @return array
* An array of arrays [MM_PERMS_READ, MM_PERMS_WRITE, MM_PERMS_SUB,
* MM_PERMS_APPLY]['groups', 'users']. All are optional. For 'groups', an
* array of gids mapped to their names is returned; for 'users' an array of
* uids is returned.
*/
function mm_content_get_perms($mmtid, $users = TRUE, $groups = TRUE, $group_names = FALSE, Connection $database = NULL) {
$empty = ['groups' => [], 'users' => []];
$out = [
Constants::MM_PERMS_WRITE => $empty,
Constants::MM_PERMS_SUB => $empty,
Constants::MM_PERMS_APPLY => $empty,
Constants::MM_PERMS_READ => $empty,
];
$database = $database ?: Database::getConnection();
if ($users) {
$select = $database->query(
'SELECT g.uid, a.mode FROM {mm_tree} t ' .
'INNER JOIN {mm_tree_access} a ON a.mmtid = t.mmtid ' .
'INNER JOIN {mm_group} g ON g.gid = a.gid ' .
'WHERE a.mmtid = :mmtid AND a.gid < 0',
[':mmtid' => $mmtid]);
foreach ($select as $row) {
$out[$row->mode]['users'][] = $row->uid;
}
}
if ($groups) {
$select = $database->query(
'SELECT a.gid, t2.name, GROUP_CONCAT(a.mode) AS modes FROM {mm_tree} t ' .
'INNER JOIN {mm_tree_access} a ON a.mmtid = t.mmtid ' .
'LEFT JOIN {mm_tree} t2 ON a.gid = t2.mmtid ' .
'WHERE a.mmtid = :mmtid AND a.gid >= 0 ' .
'GROUP BY a.gid, t2.name ' .
'ORDER BY t2.name',
[':mmtid' => $mmtid]);
foreach ($select as $row) {
foreach (explode(',', $row->modes) as $mode) {
if ($group_names) {
$out[$mode]['groups'][$row->gid] = $row->name;
}
else {
$out[$mode]['groups'][] = $row->gid;
}
}
}
}
return $out;
}
/**
* Set the permissions of an item in the MM tree
*
* @param int $mmtid
* The MM tree ID of the entry to change
* @param array $perms
* An array of arrays [MM_PERMS_READ, MM_PERMS_WRITE, MM_PERMS_SUB,
* MM_PERMS_APPLY]['groups', 'users']. All are optional. For 'groups', provide
* an array of gids; for 'users' an array of uids.
* @param bool $is_group
* TRUE if the item is a group
* @param bool $clear_old
* If TRUE, remove any existing permissions. If the calling function has only
* just created the entry, use FALSE to avoid the overhead of trying to delete
* permissions that aren't even set.
* @param Connection $database
* (optional) The database connection to use.
* @throws \Exception
* Any exception occurring during the update
*/
function mm_content_set_perms($mmtid, $perms, $is_group = FALSE, $clear_old = TRUE, Connection $database = NULL) {
foreach ([Constants::MM_PERMS_WRITE, Constants::MM_PERMS_SUB, Constants::MM_PERMS_APPLY, Constants::MM_PERMS_READ] as $m) {
if ($is_group && $m == Constants::MM_PERMS_APPLY) {
continue;
}
if (isset($perms[$m], $perms[$m]['groups'])) {
foreach ($perms[$m]['groups'] as $gid) {
if (empty($gid)) {
\Drupal::logger('mm')->error('Empty gid found when setting permissions for mmtid=@mmtid', ['@mmtid' => $mmtid]);
\Drupal::messenger()->addError(t('Permissions for this page could not be set, due to an error. Please check for groups that seem to have no name and try again.'));
return;
}
}
}
}
mm_module_invoke_all('mm_content_set_perms', $mmtid, $perms, $is_group, $clear_old);
$database = $database ?: Database::getConnection();
$use_db_query = $database->databaseType() == 'mysql';
$txn = $database->startTransaction();
try {
if ($clear_old) {
if (!$is_group) {
_mm_content_clear_access_cache($mmtid);
}
// Remove ad-hoc groups (gid<0) first.
// It's far faster to use db_query(), since DBTNG doesn't allow JOIN.
if ($use_db_query) {
$database->query('DELETE g FROM {mm_group} g INNER JOIN {mm_tree_access} a ON a.gid = g.gid WHERE a.mmtid = :mmtid AND a.gid < 0', [':mmtid' => $mmtid]);
}
else {
// DELETE FROM {mm_group} WHERE
// (SELECT 1 FROM {mm_tree_access} a WHERE a.gid = {mm_group}.gid
// AND a.mmtid = :mmtid AND a.gid < 0)
$adhoc = $database->select('mm_tree_access', 'a');
$adhoc->addExpression(1);
$adhoc->where('a.gid = {mm_group}.gid')
->condition('a.mmtid', $mmtid)
->condition('a.gid', 0, '<');
mm_retry_query($database->delete('mm_group')
->condition($adhoc));
}
// Remove everything from mm_tree_access with this mmtid
mm_retry_query($database->delete('mm_tree_access')
->condition('mmtid', $mmtid));
}
foreach ([Constants::MM_PERMS_WRITE, Constants::MM_PERMS_SUB, Constants::MM_PERMS_APPLY, Constants::MM_PERMS_READ] as $m) {
if ($is_group && $m == Constants::MM_PERMS_APPLY) {
continue;
}
if (isset($perms[$m], $perms[$m]['groups'])) {
foreach ($perms[$m]['groups'] as $gid) {
if ($gid === 'self') {
$gid = $mmtid;
}
mm_retry_query($database->insert('mm_tree_access')
->fields(['mmtid' => $mmtid, 'gid' => $gid, 'mode' => $m]));
}
}
$gid = '';
if (isset($perms[$m], $perms[$m]['users'])) {
foreach ($perms[$m]['users'] as $uid) {
_mm_content_ad_hoc_group($gid, $uid);
}
}
if ($gid !== '') {
mm_retry_query($database->insert('mm_tree_access')
->fields(['mmtid' => $mmtid, 'gid' => $gid, 'mode' => $m]));
}
}
}
catch (\Exception $e) {
$txn->rollBack();
throw $e;
}
}
/**
* Calculate the likely deletion time of an item in the recycle bin, based on
* various settings.
*
* @param int $when
* The time when the item was placed into the recycle bin, or 0 to calculate
* based on $nid or $mmtids
* @param int $nid
* (optional) Node ID of the item to be tested
* @param int|array $mmtids
* (optional) An MM Tree ID, or array of IDs, to be tested
* @param string $what
* Translated string to become part of the result
* @param bool $all_recycled
* If TRUE, only return a value if the node being tested is recycled in all
* cases, when assigned to multiple tree entries.
* @return string
* A text string describing when the content will be removed
*/
function mm_content_get_recycle_autodel_time($when, $nid, $mmtids, $what, $all_recycled = FALSE) {
$state = \Drupal::state();
$run_last = $state->get('monster_menus.cron_run_last', 0);
$run_since = $state->get('monster_menus.cron_run_since', 0);
$run_count = $state->get('monster_menus.cron_run_count', 0);
$interval = mm_get_setting('recycle_auto_empty');
if (!$run_last || !$run_since || !$run_count || $interval <= 0) {
return '';
}
if (!$when) {
if (!is_array($mmtids)) {
$mmtids = !$mmtids ? [] : [$mmtids];
}
if ($nid) {
$mmtids = array_merge($mmtids, mm_content_get_by_nid($nid));
}
$allparents = [];
foreach ($mmtids as $t) {
if (mm_content_user_can($t, Constants::MM_PERMS_IS_RECYCLED)) {
$allparents = array_merge($allparents, mm_content_get_parents_with_self($t));
}
else if ($all_recycled) {
return '';
}
}
if (!$allparents) {
return '';
}
$select = Database::getConnection()->select('mm_recycle', 'r');
$select->addExpression('MIN(r.recycle_date)');
$select->condition('r.type', 'cat')
->condition('r.id', $allparents, 'IN')
->condition('r.recycle_date', 0, '>');
$when = $select->execute()->fetchField();
}
if (!$when) {
return '';
}
$avg_run = ($run_last - $run_since) / $run_count;
$fudge = $avg_run / 100;
$next_run = $run_last + $avg_run;
if ($when + $interval - $fudge <= $next_run) {
$which_run = $next_run;
}
else {
$which_run = intval(($when + $interval - $run_since + $avg_run - 1) / $avg_run) * $avg_run + $run_since;
}
// debug_add_dump( "run_last=$run_last", "run_since=$run_since", "run_count=$run_count",
// "interval=$interval", "avg_run=$avg_run", "next_run=$next_run",
// "time=".REQUEST_TIME, "when=$when", "which_run=$which_run" );
if (($which_run = $which_run - mm_request_time()) <= 0) {
$when = t('very soon');
}
else {
$when = t('in @when', ['@when' => \Drupal::service('date.formatter')->formatInterval((int) $which_run)]);
}
return t('@what will be automatically deleted @when.', ['@what' => $what, '@when' => $when]);
}
/**
* Based on values stored in a node object, set the correct permissions. This
* function is usually called in hook_nodeapi() during the insert and update
* phases.
*
* @param NodeInterface $node
* A node object
* @throws \Exception
* Any exception occurring during the update
*/
function mm_content_set_node_perms(NodeInterface $node) {
if ($node->__get('mm_skip_perms')) {
return;
}
$everyone = $node->__get('mm_others_w_force') || \Drupal::currentUser()->hasPermission('administer all menus');
$_mmcucn_cache = &drupal_static('_mmcucn_cache');
$db = Database::getConnection();
$txn = $db->startTransaction();
try {
$nid = $node->id();
if (!empty($node->__get('others_w')) && $everyone) {
_mm_ui_delete_node_groups($node, TRUE);
mm_retry_query($db->insert('mm_node_write')
->fields(['nid' => $nid, 'gid' => 0]));
}
else {
_mm_ui_delete_node_groups($node, $everyone);
if (is_array($node->__get('groups_w'))) {
foreach ($node->__get('groups_w') as $gid => $name) {
if ($gid) {
mm_retry_query($db->insert('mm_node_write')
->fields(['nid' => $nid, 'gid' => $gid]));
}
}
}
if (is_array($node->__get('users_w'))) {
$adhoc_gid = '';
foreach ($node->__get('users_w') as $uid => $name) {
if (!empty($uid)) {
_mm_content_ad_hoc_group($adhoc_gid, $uid);
}
}
if ($adhoc_gid != '') {
mm_retry_query($db->insert('mm_node_write')
->fields(['nid' => $nid, 'gid' => $adhoc_gid]));
}
}
}
// Remove cached access rights.
unset($_mmcucn_cache[$nid]);
mm_content_clear_cache_tagged(['node' => $nid], TRUE);
}
catch (\Exception $e) {
$txn->rollBack();
// Repeat the exception, but with the location below.
throw new \Exception($e->getMessage());
}
unset($txn); // Commit
mm_content_notify_change('update_node_perms', NULL, $nid, [$nid => $node]);
}
function mm_content_test_showpage_mmtid($mmtid) {
$path = trim(mm_content_get_mmtid_url($mmtid, ['base_url' => ''])->toString(), '/');
$iter = new ContentTestShowpageIter(explode('/', $path));
mm_content_get_tree($mmtid, [Constants::MM_GET_TREE_FAST => TRUE, Constants::MM_GET_TREE_ITERATOR => $iter]);
return $iter->match;
}
/**
* Find user homepages that do not seem to have ever been modified and are older
* than a specified amount. Then, call a user-defined function to do something
* with them.
*
* @param $process_func
* A function which will process the homepages that are found. The function
* can do things like move them to the recycle bin or just calculate a count.
* It is passed one parameter: the tree object of the user page. If the
* function returns FALSE all further processing of users will be stopped.
* @param bool $consider_empty_pages
* If TRUE, test the creation/modification times of pages that are empty. If
* not, only test pages with associated nodes.
* @param int $age
* The number of seconds from the current time during which a node or page is
* considered too new to delete. The default is
* MM_UNMODIFIED_HOMEPAGES_MAX_AGE, or 30 days.
* @param int $threshold
* The number of seconds during which a user's pages and their contents must
* have been created in order for them to be considered unchanged. This
* accounts for the possibility of a homepage taking longer than a second to
* initially create. The value must be small enough that it would not be
* possible for a user to manually create a homepage and add some content to
* it within that time.
* @return int
* A count of the number of homepages that were processed.
*/
function mm_content_find_unmodified_homepages($process_func, $consider_empty_pages = FALSE, $age = Constants::MM_UNMODIFIED_HOMEPAGES_MAX_AGE, $threshold = 30) {
$iter = new ContentFindUnmodifiedHomepagesIter($process_func, $consider_empty_pages, $age, $threshold);
mm_content_get_tree(mm_content_users_mmtid(), [
Constants::MM_GET_TREE_FILTER_HIDDEN => TRUE,
Constants::MM_GET_TREE_RETURN_MTIME => TRUE,
Constants::MM_GET_TREE_VIRTUAL => FALSE,
Constants::MM_GET_TREE_ITERATOR => $iter,
]);
// Process any remaining user.
$iter->process_user();
return $iter->count;
}
/**
* Clear a Drupal cache based on tags.
*
* @param array $tags
* List of tags to invalidate.
* @param bool $local
* If TRUE, add a prefix to each tag to differentiate it from other tags of
* the same name.
*/
function mm_content_clear_cache_tagged(array $tags, $local = FALSE) {
$chunksize = 100;
$list = [];
foreach ($tags as $key => $value) {
if ($local && $key != 'mm_tree') {
$key = "_mm_$key";
}
if (is_scalar($value)) {
$list[] = "$key:$value";
}
else {
foreach (array_chunk($value, $chunksize) as $chunk) {
foreach ($chunk as $value2) {
$list[] = "$key:$value2";
}
if ($list) {
Cache::invalidateTags($list);
$list = [];
}
}
}
}
if ($list) {
Cache::invalidateTags($list);
}
}
/**
* Clear the menu routing cache.
*
* @param array $mmtids
*/
function mm_content_clear_routing_cache_tagged($mmtids = []) {
// Ideally, we would tag all resolved routes with "mm_tree:MMTID" for the
// entry and all of its parents, then invalidate based on those tags here.
// But that would require overriding router.route_provider with a new version
// of Drupal\Core\Routing::getRouteCollectionForRequest() which adds the
// correct tags to the route before setting the cache entry. Instead, clear
// all menu routes.
Cache::invalidateTags(['route_match']);
// Notes about what to do here if the above is resolved:
// $mmtids = (array) $mmtids;
// $tags = [];
// foreach ($mmtids as $mmtid) {
// $tags[] = "mm_tree:$mmtid";
// }
// Cache::invalidateTags($tags);
}
/**
* Clear the cache for one or more nodes.
*
* @param int|array $nids
* One or more Node IDs
*/
function mm_content_clear_node_cache($nids) {
mm_content_clear_cache_tagged(['node' => $nids]);
}
/**
* Clear the cache for one or more MM Tree entries.
*
* @param int|array $mmtids
* One or more MMTree IDs
*/
function mm_content_clear_page_cache($mmtids) {
mm_content_clear_cache_tagged(['mm_tree' => $mmtids]);
}
// ****************************************************************************
// * Private functions start here
// ****************************************************************************
/**
* Create a new ad-hoc group, and add users to it
*
* @param int|string &$gid
* ID of the group to add to. Before this function is called for the first
* time, set the passed parameter to ''. This function creates the group
* entry, adds the first user to it, and sets $gid to the ID of the new group.
* Later iterations reuse this ID.
* @param int $uid
* User ID to store in the group
*/
function _mm_content_ad_hoc_group(&$gid, $uid) {
$db = Database::getConnection();
if ($gid === '') {
// First time, so get the next gid to use.
// Start a transaction, so $gid is valid until this function returns
$txn = $db->startTransaction();
$gid = $db->query('SELECT LEAST(-1, MIN(gid) - 1) FROM {mm_group}')->fetchField();
if (empty($gid)) {
$gid = -1;
}
}
mm_retry_query($db->insert('mm_group')
->fields(['gid' => $gid, 'uid' => $uid]));
}
function _mm_content_virtual_dir($mmtid, $par, $level, $state) {
$ch = chr(-$mmtid);
if (!ctype_alpha($ch)) {
$name = function_exists('t') ? t('(other)') : '(other)';
$alias = '~';
}
else {
$name = $alias = $ch;
}
$perms = mm_content_user_can($par);
$perms[Constants::MM_PERMS_WRITE] = $perms[Constants::MM_PERMS_APPLY] = FALSE;
return (object) [
'name' => $name,
'alias' => $alias,
'mmtid' => $mmtid,
'parent' => $par,
'uid' => 1,
'default_mode' => Constants::MM_PERMS_READ,
'bid' => '',
'max_depth' => -1,
'max_parents' => -1,
'perms' => $perms,
'level' => $level,
'is_group' => FALSE,
'is_user' => TRUE,
'is_dot' => FALSE,
'is_virtual' => TRUE,
'state' => $state,
];
}
/**
* Create a new recycling bin, or return the tree ID of the existing one
*
* @param int $mmtid
* The tree ID in which to create the recycle bin. If 0, the user's home
* directory is used or, if possible, the top level of the tree.
* @return int|string
* Either the tree ID of the recycling bin or an error string. Use
* is_numeric() to evaluate.
*/
function _mm_content_make_recycle($mmtid = 0) {
$user = \Drupal::currentUser();
if (!$mmtid) {
if (!empty($user->user_mmtid) && mm_content_user_can($user->user_mmtid, Constants::MM_PERMS_SUB)) {
$mmtid = $user->user_mmtid;
}
elseif (mm_content_user_can(mm_home_mmtid(), Constants::MM_PERMS_SUB)) {
$mmtid = mm_home_mmtid();
}
else {
return t('Could not create a recycle bin');
}
}
$found = mm_content_get(['parent' => $mmtid, 'name' => Constants::MM_ENTRY_NAME_RECYCLE]);
if ($found) {
return $found[0]->mmtid;
}
return mm_content_insert_or_update(TRUE, $mmtid, ['name' => Constants::MM_ENTRY_NAME_RECYCLE, 'alias' => t('-recycle'), 'uid' => $user->id()]);
}
function _mm_content_comments_readable(NodeInterface $node) {
if (mm_get_setting('comments.finegrain_readability')) {
$perm = empty($node->__get('comments_readable')) ? Constants::MM_COMMENT_READABILITY_DEFAULT : $node->__get('comments_readable');
return \Drupal::currentUser()->hasPermission($perm);
}
return \Drupal::currentUser()->hasPermission('access comments');
}
function _mm_content_access_cache($cid, $data = NULL, $uid = NULL, $nid = NULL, $mmtid = NULL) {
if ($cache_time = mm_get_setting('access_cache_time')) {
$obj = \Drupal::cache('mm_access');
if (!empty($data)) {
$add_page_tags = function ($mmtid) use (&$tags) {
foreach (mm_content_get_parents_with_self($mmtid, FALSE, FALSE) as $page) {
if ($page > 1) {
$tags[] = "mm_tree:$page";
}
}
};
$tags = [];
if (!is_null($uid)) {
// Allow this entry to be invalidated by either our own internal tag
// or the global 'user' tag. This way we can choose to invalidate just
// our data without also invalidating entries in all other caches.
$tags[] = "user:$uid";
$tags[] = "_mm_user:$uid";
}
if (!empty($nid)) {
// Allow this entry to be invalidated by either our own internal tag
// or the global 'node' tag.
$tags[] = "node:$nid";
$tags[] = "_mm_node:$nid";
// Add tags for all pages on which this node appears.
foreach (mm_content_get_by_nid($nid) as $node_mmtid) {
$add_page_tags($node_mmtid);
}
}
else if (!empty($mmtid)) {
$add_page_tags($mmtid);
}
$expire = mm_request_time() + $cache_time;
return $obj->set($cid, $data, $expire, $tags);
}
elseif ($cache = $obj->get($cid)) {
if ($cache->expire > mm_request_time()) {
return $cache->data;
}
}
}
return FALSE;
}
function _mm_content_clear_access_cache($mmtid = NULL) {
static $list = [];
// Make a list of pages whose permissions or location in the tree have
// changed, then remove entries from cache.mm_access for all nodes appearing
// on these pages and their children during exit.
if (!empty($mmtid)) {
foreach ((array) $mmtid as $m) {
$list[$m] = TRUE;
}
}
elseif ($list) {
// Get the list of pages whose permissions or location in the tree have
// changed and remove entries from cache.mm_access for all nodes appearing
// on these pages and their children.
mm_content_clear_page_cache(array_keys($list));
$list = [];
}
}
