diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js index bcaa8d106..9437ffccd 100644 --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -1,186 +1,192 @@ // @flow import PropTypes from 'prop-types'; import invariant from 'invariant'; export type VisibilityRules = 0 | 1 | 2 | 3 | 4; export const visibilityRules = { OPEN: 0, CLOSED: 1, SECRET: 2, CHAT_NESTED_OPEN: 3, CHAT_SECRET: 4, }; export function assertVisibilityRules( ourVisibilityRules: number, ): VisibilityRules { invariant( ourVisibilityRules === 0 || ourVisibilityRules === 1 || ourVisibilityRules === 2 || ourVisibilityRules === 3 || ourVisibilityRules === 4, "number is not visibilityRules enum", ); return ourVisibilityRules; } export type EditRules = 0 | 1; export const editRules = { ANYBODY: 0, LOGGED_IN: 1, }; export function assertEditRules( ourEditRules: number, ): EditRules { invariant( ourEditRules === 0 || ourEditRules === 1, "number is not editRules enum", ); return ourEditRules; } // Keep in sync with server/permissions.php export type ThreadPermission = | "know_of" | "visible" | "voiced" | "edit_entries" | "edit_thread" | "delete_thread" | "create_subthreads" | "join_thread" | "edit_permissions" - | "add_members"; + | "add_members" + | "remove_members" + | "change_role"; export const threadPermissions = { KNOW_OF: "know_of", VISIBLE: "visible", VOICED: "voiced", EDIT_ENTRIES: "edit_entries", EDIT_THREAD: "edit_thread", DELETE_THREAD: "delete_thread", CREATE_SUBTHREADS: "create_subthreads", JOIN_THREAD: "join_thread", EDIT_PERMISSIONS: "edit_permissions", ADD_MEMBERS: "add_members", + REMOVE_MEMBERS: "remove_members", + CHANGE_ROLE: "change_role", }; export function assertThreadPermissions( ourThreadPermissions: string, ): ThreadPermission { invariant( ourThreadPermissions === "know_of" || ourThreadPermissions === "visible" || ourThreadPermissions === "voiced" || ourThreadPermissions === "edit_entries" || ourThreadPermissions === "edit_thread" || ourThreadPermissions === "delete_thread" || ourThreadPermissions === "create_subthreads" || ourThreadPermissions === "join_thread" || ourThreadPermissions === "edit_permissions" || - ourThreadPermissions === "add_members", + ourThreadPermissions === "add_members" || + ourThreadPermissions === "remove_members" || + ourThreadPermissions === "change_role", "string is not threadPermissions enum", ); return ourThreadPermissions; } type ThreadPermissionInfo = | {| value: true, source: string |} | {| value: false, source: null |}; export type ThreadPermissionsBlob = {| [permission: ThreadPermission]: ThreadPermissionInfo, |}; export const threadPermissionsBlobPropType = PropTypes.objectOf( PropTypes.oneOfType([ PropTypes.shape({ value: PropTypes.oneOf([ true ]), source: PropTypes.string.isRequired, }), PropTypes.shape({ value: PropTypes.oneOf([ false ]), source: PropTypes.oneOf([ null ]), }), ]), ); export type MemberInfo = {| id: string, role: ?string, permissions: ThreadPermissionsBlob, |}; export const memberInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, role: PropTypes.string, permissions: threadPermissionsBlobPropType.isRequired, }); export type RelativeMemberInfo = {| ...MemberInfo, username: ?string, isViewer: bool, |}; export const relativeMemberInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, role: PropTypes.string, permissions: threadPermissionsBlobPropType.isRequired, username: PropTypes.string, isViewer: PropTypes.bool.isRequired, }); export type RoleInfo = {| id: string, name: string, // This is not a ThreadPermissionsBlob. It can include keys with prefixes, and // no sources are listed, as this blob is itself a self-contained source. permissions: {| [permission: string]: bool |}, isDefault: bool, |}; export type ThreadCurrentUserInfo = {| role: ?string, subscribed: bool, permissions: ThreadPermissionsBlob, |}; export type ThreadInfo = {| id: string, name: string, description: string, visibilityRules: VisibilityRules, color: string, // hex, without "#" or "0x" editRules: EditRules, creationTime: number, // millisecond timestamp parentThreadID: ?string, members: MemberInfo[], roles: {[id: string]: RoleInfo}, currentUser: ThreadCurrentUserInfo, |}; export const visibilityRulesPropType = PropTypes.oneOf([ 0, 1, 2, 3, 4, ]); export const threadInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, description: PropTypes.string.isRequired, visibilityRules: visibilityRulesPropType.isRequired, color: PropTypes.string.isRequired, editRules: PropTypes.oneOf([ 0, 1, ]).isRequired, creationTime: PropTypes.number.isRequired, parentThreadID: PropTypes.string, members: PropTypes.arrayOf(memberInfoPropType).isRequired, roles: PropTypes.objectOf(PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, permissions: PropTypes.objectOf(PropTypes.bool).isRequired, isDefault: PropTypes.bool.isRequired, })), currentUser: PropTypes.shape({ role: PropTypes.string, subscribed: PropTypes.bool.isRequired, permissions: threadPermissionsBlobPropType.isRequired, }), }); diff --git a/native/chat/settings/thread-settings-user.react.js b/native/chat/settings/thread-settings-user.react.js index 7f88f102a..28ef74071 100644 --- a/native/chat/settings/thread-settings-user.react.js +++ b/native/chat/settings/thread-settings-user.react.js @@ -1,179 +1,199 @@ // @flow import type { ThreadInfo, RelativeMemberInfo } from 'lib/types/thread-types'; import { threadInfoPropType, threadPermissions, relativeMemberInfoPropType, } from 'lib/types/thread-types'; import React from 'react'; import { View, Text, StyleSheet, Platform } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; import PropTypes from 'prop-types'; import _isEqual from 'lodash/fp/isEqual'; import { threadHasPermission } from 'lib/shared/thread-utils'; import { stringForUser } from 'lib/shared/user-utils'; import EditSettingButton from './edit-setting-button.react'; import Button from '../../components/button.react'; import PopoverTooltip from '../../components/popover-tooltip.react'; type Props = {| memberInfo: RelativeMemberInfo, threadInfo: ThreadInfo, canEdit: bool, |}; type State = {| popoverConfig: $ReadOnlyArray<{ label: string, onPress: () => void }>, |}; class ThreadSettingsUser extends React.PureComponent { static propTypes = { memberInfo: relativeMemberInfoPropType.isRequired, threadInfo: threadInfoPropType.isRequired, canEdit: PropTypes.bool.isRequired, }; static memberIsAdmin(props: Props) { const role = props.memberInfo.role && props.threadInfo.roles[props.memberInfo.role]; return role && !role.isDefault && role.name === "Admins"; } generatePopoverConfig(props: Props) { - // TODO check correct permissions - const canEditThread = threadHasPermission( - props.threadInfo, - threadPermissions.EDIT_THREAD, - ); - if (!canEditThread || !props.canEdit) { + const role = props.memberInfo.role; + if (!props.canEdit || !role) { return []; } + + const canRemoveMembers = threadHasPermission( + props.threadInfo, + threadPermissions.REMOVE_MEMBERS, + ); + const canChangeRoles = threadHasPermission( + props.threadInfo, + threadPermissions.CHANGE_ROLE, + ); + const result = []; - if (!props.memberInfo.isViewer) { + if ( + canRemoveMembers && + !props.memberInfo.isViewer && + ( + canChangeRoles || + ( + props.threadInfo.roles[role] && + props.threadInfo.roles[role].isDefault + ) + ) + ) { result.push({ label: "Remove user", onPress: this.onPressRemoveUser }); } - const adminText = ThreadSettingsUser.memberIsAdmin(props) - ? "Remove admin" - : "Make admin"; - result.push({ label: adminText, onPress: this.onPressMakeAdmin }); + + if (canChangeRoles) { + const adminText = ThreadSettingsUser.memberIsAdmin(props) + ? "Remove admin" + : "Make admin"; + result.push({ label: adminText, onPress: this.onPressMakeAdmin }); + } + return result; } constructor(props: Props) { super(props); this.state = { popoverConfig: this.generatePopoverConfig(props), }; } componentWillReceiveProps(nextProps: Props) { const nextPopoverConfig = this.generatePopoverConfig(nextProps); if (!_isEqual(this.state.popoverConfig)(nextPopoverConfig)) { this.setState({ popoverConfig: nextPopoverConfig }); } } render() { const userText = stringForUser(this.props.memberInfo); let userInfo = null; if (this.props.memberInfo.username) { userInfo = ( {userText} ); } else { userInfo = ( {userText} ); } let editButton = null; if (this.state.popoverConfig.length !== 0) { editButton = ( ); } let roleInfo = null; if (ThreadSettingsUser.memberIsAdmin(this.props)) { roleInfo = ( admin ); } return ( {userInfo} {editButton} {roleInfo} ); } onPressRemoveUser = () => { } onPressMakeAdmin = () => { } } const styles = StyleSheet.create({ container: { flex: 1, paddingVertical: 8, paddingHorizontal: 12, }, row: { flex: 1, flexDirection: 'row', }, username: { flex: 1, fontSize: 16, color: "#333333", }, anonymous: { fontStyle: 'italic', color: "#888888", }, editIcon: { lineHeight: 20, paddingLeft: 10, paddingTop: Platform.select({ android: 1, default: 0 }), textAlign: 'right', }, popoverLabelStyle: { textAlign: 'center', color: '#444', }, role: { flex: 1, fontSize: 14, color: "#888888", paddingTop: 4, }, }); const icon = ( ); export default ThreadSettingsUser; diff --git a/server/permissions.php b/server/permissions.php index 1d18f1c70..8b9edaed6 100644 --- a/server/permissions.php +++ b/server/permissions.php @@ -1,836 +1,848 @@ query($query); $row = $result->fetch_assoc(); if (!$row) { return false; } return (int)$row['roletype'] !== 0; } function permission_lookup($blob, $permission) { if (!$blob || !isset($blob[$permission])) { return false; } return (bool)$blob[$permission]['value']; } // $info should include: // - permissions: ?array // - visibility_rules: int // - edit_rules: int function permission_helper($info, $permission) { if (!$info) { return null; } $vis_rules = $info['visibility_rules']; if ( ($permission === PERMISSION_KNOW_OF && $vis_rules === VISIBILITY_OPEN) || ($permission === PERMISSION_KNOW_OF && $vis_rules === VISIBILITY_CLOSED) || ($permission === PERMISSION_VISIBLE && $vis_rules === VISIBILITY_OPEN) || ($permission === PERMISSION_JOIN_THREAD && ( $vis_rules === VISIBILITY_OPEN || $vis_rules === VISIBILITY_CLOSED || // with closed or secret, you also $vis_rules === VISIBILITY_SECRET // need to know the thread password )) ) { return true; } else if ( $permission === PERMISSION_EDIT_ENTRIES && ( $vis_rules === VISIBILITY_OPEN || $vis_rules === VISIBILITY_CLOSED || $vis_rules === VISIBILITY_SECRET ) ) { // The legacy visibility classes have functionality where you can play // around with them on web without being logged in. This allows anybody // that passes a visibility check to edit the calendar entries of a thread, // regardless of membership in that thread. Depending on edit_rules, the // ability may be restricted to only logged in users. $lookup = permission_lookup($info['permissions'], $permission); if ($lookup) { return true; } $can_view = permission_helper($info, PERMISSION_VISIBLE); if (!$can_view) { return false; } if ($info['edit_rules'] === EDIT_LOGGED_IN) { return user_logged_in(); } return true; } return permission_lookup($info['permissions'], $permission); } function get_info_from_permissions_row($row) { $blob = null; if ($row['permissions']) { $decoded = json_decode($row['permissions'], true); if (gettype($decoded) === "array") { $blob = $decoded; } } return array( "permissions" => $blob, "visibility_rules" => (int)$row['visibility_rules'], "edit_rules" => (int)$row['edit_rules'], "roletype" => $row['roletype'] !== null ? (int)$row['roletype'] : null, ); } // null if thread does not exist function fetch_thread_permission_info($thread) { global $conn; $viewer_id = get_viewer_id(); $query = <<query($query); $row = $result->fetch_assoc(); if (!$row) { return null; } return get_info_from_permissions_row($row); } // null if thread does not exist function check_thread_permission($thread, $permission) { $info = fetch_thread_permission_info($thread); return permission_helper($info, $permission); } function get_all_thread_permissions($info, $thread_id) { global $all_thread_permissions; $return = array(); foreach ($all_thread_permissions as $permission) { $result = permission_helper($info, $permission); $source = null; if ($result) { if ($info && $info['permissions'] && $info['permissions'][$permission]) { $source = (string)$info['permissions'][$permission]['source']; } else { $source = (string)$thread_id; } } $return[$permission] = array( 'value' => $result, 'source' => $source, ); } return $return; } // null if entry does not exist function check_thread_permission_for_entry($entry, $permission) { global $conn; $viewer_id = get_viewer_id(); $query = <<query($query); $row = $result->fetch_assoc(); if (!$row || $row['visibility_rules'] === null) { return null; } $info = get_info_from_permissions_row($row); return permission_helper($info, $permission); } // $roletype_permissions: ?array // can be null if roletype = 0 // $permissions_from_parent: // ?array bool, source => int)> // can be null if no permissions from parent (should never be empty array) // $thread_id: int // $vis_rules: int // return: ?array bool, source => int)> // can be null if no permissions exist function make_permissions_blob( $roletype_permissions, $permissions_from_parent, $thread_id, $vis_rules ) { $permissions = array(); if ($permissions_from_parent) { foreach ($permissions_from_parent as $permission => $pair) { if ( !vis_rules_are_open($vis_rules) && ( strpos($permission, PERMISSION_PREFIX_OPEN_DESCENDANT) === 0 || strpos($permission, PERMISSION_PREFIX_OPEN) === 0 ) ) { continue; } if (strpos($permission, PERMISSION_PREFIX_OPEN) === 0) { $permissions[substr($permission, 5)] = $pair; continue; } $permissions[$permission] = $pair; } } if ($roletype_permissions) { foreach ($roletype_permissions as $permission => $value) { $current_pair = isset($permissions[$permission]) ? $permissions[$permission] : null; if ($value || (!$value && (!$current_pair || !$current_pair['value']))) { $permissions[$permission] = array( 'value' => $value, 'source' => $thread_id, ); } } } if (!$permissions) { return null; } return $permissions; } // $permissions: ?array bool, source => int)> // can be null if make_permissions_blob returns null // return: ?array bool, source => int)> // can be null if $permissions is null, or if no permissions go to children function permissions_for_children($permissions) { if (!$permissions) { return null; } $permissions_for_children = array(); foreach ($permissions as $permission => $pair) { if (strpos($permission, PERMISSION_PREFIX_DESCENDANT) === 0) { $permissions_for_children[$permission] = $pair; $permissions_for_children[substr($permission, 11)] = $pair; } else if (strpos($permission, PERMISSION_PREFIX_CHILD) === 0) { $permissions_for_children[substr($permission, 6)] = $pair; } } if (!$permissions_for_children) { return null; } return $permissions_for_children; } // $to_save: array bool, source => int)> // permissions_for_children: // ?array bool, source => int)> // roletype: int, // subscribed?: bool, // )> function save_user_roles($to_save) { global $conn; if (!$to_save) { return; } $time = round(microtime(true) * 1000); // in milliseconds $new_row_sql_strings = array(); foreach ($to_save as $role_info) { $permissions = "'" . $conn->real_escape_string(json_encode( $role_info['permissions'], JSON_FORCE_OBJECT )) . "'"; $permissions_for_children = "NULL"; if ($role_info['permissions_for_children']) { $permissions_for_children = "'" . $conn->real_escape_string(json_encode( $role_info['permissions_for_children'] )) . "'"; } $visible = isset($role_info['permissions'][PERMISSION_VISIBLE]['value']) ? ($role_info['permissions'][PERMISSION_VISIBLE]['value'] ? "1" : "0") : "0"; $subscribed = isset($role_info['subscribed']) ? ($role_info['subscribed'] ? "1" : "0") : "0"; $new_row_sql_strings[] = "(" . implode(", ", array( $role_info['user_id'], $role_info['thread_id'], $role_info['roletype'], $time, $subscribed, $permissions, $permissions_for_children, $visible, )) . ")"; } $new_rows_sql_string = implode(", ", $new_row_sql_strings); // Logic below will only update an existing role row's `subscribed` column if // the user is either leaving or joining the thread. Generally, joining means // you subscribe and leaving means you unsubscribe. $query = <<query($query); } // $to_delete: array function delete_user_roles($to_delete) { global $conn; if (!$to_delete) { return; } $delete_row_sql_strings = array(); foreach ($to_delete as $role_info) { $user = $role_info['user_id']; $thread = $role_info['thread_id']; $delete_row_sql_strings[] = "(user = {$user} AND thread = {$thread})"; } $delete_rows_sql_string = implode(" OR ", $delete_row_sql_strings); $query = <<query($query); } // $initial_parent_thread_id: int // $initial_users_to_permissions_from_parent: // array< // user_id: int, // array bool, source => int)>, // > // returns: // array( // to_save => array bool, source => int)> // permissions_for_children: // ?array bool, source => int)> // roletype: int, // )>, // to_delete: array, // ) function update_descendant_permissions( $initial_parent_thread_id, $initial_users_to_permissions_from_parent ) { global $conn; $stack = array(array( $initial_parent_thread_id, $initial_users_to_permissions_from_parent, )); $to_save = array(); $to_delete = array(); while ($stack) { list($parent_thread_id, $users_to_permissions_from_parent) = array_shift($stack); $user_ids = array_keys($users_to_permissions_from_parent); $user_id_sql_string = implode(", ", $user_ids); $query = <<query($query); $child_thread_infos = array(); while ($row = $result->fetch_assoc()) { $thread_id = (int)$row['id']; if (!isset($child_thread_infos[$thread_id])) { $child_thread_infos[$thread_id] = array( "visibility_rules" => (int)$row['visibility_rules'], "user_infos" => array(), ); } if (!$row['user']) { continue; } $user_id = (int)$row['user']; $child_thread_infos[$thread_id]["user_infos"][$user_id] = array( "roletype" => (int)$row['roletype'], "roletype_permissions" => json_decode($row['roletype_permissions'], true), "permissions" => json_decode($row['permissions'], true), "permissions_for_children" => json_decode($row['permissions_for_children'], true), ); } foreach ($child_thread_infos as $thread_id => $child_thread_info) { $user_infos = $child_thread_info['user_infos']; $users_for_next_layer = array(); foreach ( $users_to_permissions_from_parent as $user_id => $permissions_from_parent ) { $roletype = 0; $roletype_permissions = null; $old_permissions = null; $old_permissions_for_children = null; if (isset($user_infos[$user_id])) { $roletype = $user_infos[$user_id]['roletype']; $roletype_permissions = $user_infos[$user_id]['roletype_permissions']; $old_permissions = $user_infos[$user_id]['permissions']; $old_permissions_for_children = $user_infos[$user_id]['permissions_for_children']; } $permissions = make_permissions_blob( $roletype_permissions, $permissions_from_parent, $thread_id, $child_thread_info['visibility_rules'] ); if ($permissions == $old_permissions) { // This thread and all of its children need no updates, since its // permissions are unchanged by this operation continue; } $permissions_for_children = permissions_for_children($permissions); if ($permissions !== null) { $to_save[] = array( "user_id" => $user_id, "thread_id" => $thread_id, "permissions" => $permissions, "permissions_for_children" => $permissions_for_children, "roletype" => $roletype, ); } else { $to_delete[] = array( "user_id" => $user_id, "thread_id" => $thread_id, ); } if ($permissions_for_children != $old_permissions_for_children) { // Our children only need updates if permissions_for_children changed $users_for_next_layer[$user_id] = $permissions_for_children; } } if ($users_for_next_layer) { $stack[] = array($thread_id, $users_for_next_layer); } } } return array("to_save" => $to_save, "to_delete" => $to_delete); } // $thread_id: int // $user_ids: array // $roletype: ?int // if nonzero integer, the ID of the corresponding roletype row // if zero, indicates that $user_id is not a member of $thread_id // if null, $user_id's roletype will be set to $thread_id's default_roletype, // but only if they don't already have a nonzero roletype. this is useful // for adding people to threads // returns: (null if failed) // ?array( // to_save => array bool, source => int)> // permissions_for_children: // ?array bool, source => int)> // roletype: int, // )>, // to_delete: array, // ) function change_roletype($thread_id, $user_ids, $roletype) { global $conn; // The code in the blocks below needs to determine three variables: // - $new_roletype, the actual $roletype value we're saving // - $roletype_permissions, the permissions column of the $new_roletype // - $vis_rules, the visibility rules of $thread_id if ($roletype === 0) { $new_roletype = 0; $roletype_permissions = null; $query = <<query($query); $row = $result->fetch_assoc(); if (!$row) { return null; } $vis_rules = (int)$row['visibility_rules']; } else if ($roletype !== null) { $new_roletype = (int)$roletype; $query = <<query($query); $row = $result->fetch_assoc(); if (!$row) { return null; } $roletype_permissions = json_decode($row['permissions'], true); $vis_rules = (int)$row['visibility_rules']; } else { $query = <<query($query); $row = $result->fetch_assoc(); if (!$row) { return null; } $new_roletype = (int)$row['default_roletype']; $roletype_permissions = json_decode($row['permissions'], true); $vis_rules = (int)$row['visibility_rules']; } $user_id_sql_string = implode(", ", $user_ids); $query = <<query($query); $role_info = array(); while ($row = $result->fetch_assoc()) { $user_id = (int)$row['user']; $old_permissions_for_children = $row['permissions_for_children'] ? json_decode($row['permissions_for_children'], true) : null; $permissions_from_parent = $row['permissions_from_parent'] ? json_decode($row['permissions_from_parent'], true) : null; $role_info[$user_id] = array( "old_roletype" => (int)$row['roletype'], "old_permissions_for_children" => $old_permissions_for_children, "permissions_from_parent" => $permissions_from_parent, ); } $to_save = array(); $to_delete = array(); $to_update_descendants = array(); foreach ($user_ids as $user_id) { $old_permissions_for_children = null; $permissions_from_parent = null; if (isset($role_info[$user_id])) { $old_roletype = $role_info[$user_id]['old_roletype']; if ($old_roletype === $new_roletype) { // If the old roletype is the same as the new one, we have nothing to // update continue; } else if ($old_roletype !== 0 && $roletype === null) { // In the case where we're just trying to add somebody to a thread, if // they already have a role with a nonzero roletype then we don't need // to do anything continue; } $old_permissions_for_children = $role_info[$user_id]['old_permissions_for_children']; $permissions_from_parent = $role_info[$user_id]['permissions_from_parent']; } $permissions = make_permissions_blob( $roletype_permissions, $permissions_from_parent, $thread_id, $vis_rules ); $permissions_for_children = permissions_for_children($permissions); if ($permissions === null) { $to_delete[] = array("user_id" => $user_id, "thread_id" => $thread_id); } else { $to_save[] = array( "user_id" => $user_id, "thread_id" => $thread_id, "permissions" => $permissions, "permissions_for_children" => $permissions_for_children, "roletype" => $new_roletype, ); } if ($permissions_for_children != $old_permissions_for_children) { $to_update_descendants[$user_id] = $permissions_for_children; } } if ($to_update_descendants) { $descendant_results = update_descendant_permissions($thread_id, $to_update_descendants); $to_save = array_merge($to_save, $descendant_results['to_save']); $to_delete = array_merge($to_delete, $descendant_results['to_delete']); } return array("to_save" => $to_save, "to_delete" => $to_delete); } // $thread_id: int // $new_vis_rules: int // note: doesn't check if the new value is different from the old value // returns: // array( // to_save => array bool, source => int)> // permissions_for_children: // ?array bool, source => int)> // roletype: int, // )>, // to_delete: array, // ) function recalculate_all_permissions($thread_id, $new_vis_rules) { global $conn; $new_vis_rules = (int)$new_vis_rules; $thread_id = (int)$thread_id; $query = <<query($query); $query = <<query($query); $to_save = array(); $to_delete = array(); $to_update_descendants = array(); while ($row = $result->fetch_assoc()) { $user_id = (int)$row['user']; $roletype = (int)$row['roletype']; $old_permissions = json_decode($row['permissions'], true); $old_permissions_for_children = $row['permissions_for_children'] ? json_decode($row['permissions_for_children'], true) : null; $permissions_from_parent = $row['permissions_from_parent'] ? json_decode($row['permissions_from_parent'], true) : null; $roletype_permissions = $row['roletype_permissions'] ? json_decode($row['roletype_permissions'], true) : null; $permissions = make_permissions_blob( $roletype_permissions, $permissions_from_parent, $thread_id, $new_vis_rules ); if ($permissions == $old_permissions) { // This thread and all of its children need no updates, since its // permissions are unchanged by this operation continue; } $permissions_for_children = permissions_for_children($permissions); if ($permissions !== null) { $to_save[] = array( "user_id" => $user_id, "thread_id" => $thread_id, "permissions" => $permissions, "permissions_for_children" => $permissions_for_children, "roletype" => $roletype, ); } else { $to_delete[] = array( "user_id" => $user_id, "thread_id" => $thread_id, ); } if ($permissions_for_children != $old_permissions_for_children) { $to_update_descendants[$user_id] = $permissions_for_children; } } if ($to_update_descendants) { $descendant_results = update_descendant_permissions($thread_id, $to_update_descendants); $to_save = array_merge($to_save, $descendant_results['to_save']); $to_delete = array_merge($to_delete, $descendant_results['to_delete']); } return array("to_save" => $to_save, "to_delete" => $to_delete); } function create_initial_roletypes_for_new_thread($thread_id) { global $conn; $conn->query("INSERT INTO ids(table_name) VALUES('roletypes')"); $member_roletype_id = $conn->insert_id; $conn->query("INSERT INTO ids(table_name) VALUES('roletypes')"); $admin_roletype_id = $conn->insert_id; $member_permissions = array( PERMISSION_KNOW_OF => true, PERMISSION_VISIBLE => true, PERMISSION_JOIN_THREAD => true, PERMISSION_PREFIX_OPEN_DESCENDANT . PERMISSION_KNOW_OF => true, PERMISSION_PREFIX_OPEN_DESCENDANT . PERMISSION_VISIBLE => true, PERMISSION_PREFIX_OPEN_DESCENDANT . PERMISSION_JOIN_THREAD => true, PERMISSION_VOICED => true, PERMISSION_EDIT_ENTRIES => true, PERMISSION_EDIT_THREAD => true, PERMISSION_CREATE_SUBTHREADS => true, PERMISSION_ADD_MEMBERS => true, ); $admin_permissions = array( PERMISSION_KNOW_OF => true, PERMISSION_VISIBLE => true, PERMISSION_JOIN_THREAD => true, PERMISSION_VOICED => true, PERMISSION_EDIT_ENTRIES => true, PERMISSION_EDIT_THREAD => true, PERMISSION_CREATE_SUBTHREADS => true, PERMISSION_ADD_MEMBERS => true, PERMISSION_DELETE_THREAD => true, PERMISSION_EDIT_PERMISSIONS => true, + PERMISSION_REMOVE_MEMBERS => true, + PERMISSION_CHANGE_ROLE => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_KNOW_OF => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_VISIBLE => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_JOIN_THREAD => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_VOICED => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_EDIT_ENTRIES => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_EDIT_THREAD => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_CREATE_SUBTHREADS => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_ADD_MEMBERS => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_DELETE_THREAD => true, PERMISSION_PREFIX_DESCENDANT . PERMISSION_EDIT_PERMISSIONS => true, + PERMISSION_PREFIX_DESCENDANT . PERMISSION_REMOVE_MEMBERS => true, + PERMISSION_PREFIX_DESCENDANT . PERMISSION_CHANGE_ROLE => true, ); $encoded_member_permissions = $conn->real_escape_string(json_encode( $member_permissions )); $encoded_admin_permissions = $conn->real_escape_string(json_encode( $admin_permissions )); $time = round(microtime(true) * 1000); // in milliseconds $query = <<query($query); return array( "members" => array( "id" => (string)$member_roletype_id, "name" => "Members", "permissions" => $member_permissions, "isDefault" => true, ), "admins" => array( "id" => (string)$admin_roletype_id, "name" => "Admins", "permissions" => $admin_permissions, "isDefault" => false, ), ); } diff --git a/web/dist/prod.build.js b/web/dist/prod.build.js index 04921b282..9576a2c77 100644 --- a/web/dist/prod.build.js +++ b/web/dist/prod.build.js @@ -1,85 +1,85 @@ !function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=448)}([function(e,t,n){var r=n(7),o=n(50),a=n(34),i=n(35),u=n(46),s=function(e,t,n){var c,l,f,p,d=e&s.F,h=e&s.G,v=e&s.S,y=e&s.P,m=e&s.B,g=h?r:v?r[t]||(r[t]={}):(r[t]||{}).prototype,b=h?o:o[t]||(o[t]={}),w=b.prototype||(b.prototype={});h&&(n=t);for(c in n)l=!d&&g&&void 0!==g[c],f=(l?g:n)[c],p=m&&l?u(f,r):y&&"function"==typeof f?u(Function.call,f):f,g&&i(g,c,f,e&s.U),b[c]!=f&&a(b,c,p),y&&w[c]!=f&&(w[c]=f)};r.core=o,s.F=1,s.G=2,s.S=4,s.P=8,s.B=16,s.W=32,s.U=64,s.R=128,e.exports=s},function(e,t,n){"use strict";e.exports=n(92)},function(e,t,n){e.exports=n(746)()},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0}),t.ReactCSS=t.loop=t.handleActive=t.handleHover=t.hover=void 0;var o=n(997),a=r(o),i=n(998),u=r(i),s=n(1e3),c=r(s),l=n(1001),f=r(l),p=n(1002),d=r(p),h=n(1003),v=r(h);t.hover=f.default,t.handleHover=f.default,t.handleActive=d.default,t.loop=v.default;var y=t.ReactCSS=function(e){for(var t=arguments.length,n=Array(t>1?t-1:0),r=1;r0?o(r(e),9007199254740991):0}},function(e,t,n){"use strict";function r(e,t){return 1===e.nodeType&&e.getAttribute(h)===String(t)||8===e.nodeType&&e.nodeValue===" react-text: "+t+" "||8===e.nodeType&&e.nodeValue===" react-empty: "+t+" "}function o(e){for(var t;t=e._renderedComponent;)e=t;return e}function a(e,t){var n=o(e);n._hostNode=t,t[y]=n}function i(e){var t=e._hostNode;t&&(delete t[y],e._hostNode=null)}function u(e,t){if(!(e._flags&v.hasCachedChildNodes)){var n=e._renderedChildren,i=t.firstChild;e:for(var u in n)if(n.hasOwnProperty(u)){var s=n[u],c=o(s)._domID;if(0!==c){for(;null!==i;i=i.nextSibling)if(r(i,c)){a(s,i);continue e}f("32",c)}}e._flags|=v.hasCachedChildNodes}}function s(e){if(e[y])return e[y];for(var t=[];!e[y];){if(t.push(e),!e.parentNode)return null;e=e.parentNode}for(var n,r;e&&(r=e[y]);e=t.pop())n=r,t.length&&u(r,e);return n}function c(e){var t=s(e);return null!=t&&t._hostNode===e?t:null}function l(e){if(void 0===e._hostNode&&f("33"),e._hostNode)return e._hostNode;for(var t=[];!e._hostNode;)t.push(e),e._hostParent||f("34"),e=e._hostParent;for(;t.length;e=t.pop())u(e,e._hostNode);return e._hostNode}var f=n(11),p=n(94),d=n(319),h=(n(4),p.ID_ATTRIBUTE_NAME),v=d,y="__reactInternalInstance$"+Math.random().toString(36).slice(2),m={getClosestInstanceFromNode:s,getInstanceFromNode:c,getNodeFromInstance:l,precacheChildNodes:u,precacheNode:a,uncacheNode:i};e.exports=m},function(e,t,n){"use strict";var r=n(745),o=(n(346),n(748));n.d(t,"a",function(){return r.a}),n.d(t,"b",function(){return o.a})},function(e,t,n){"use strict";var r=function(e,t,n,r,o,a,i,u){if(!e){var s;if(void 0===t)s=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,o,a,i,u],l=0;s=new Error(t.replace(/%s/g,function(){return c[l++]})),s.name="Invariant Violation"}throw s.framesToPop=1,s}};e.exports=r},function(e,t,n){var r=n(52);e.exports=function(e){return Object(r(e))}},function(e,t,n){"use strict";var r=function(){};e.exports=r},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t,n){var r=n(366),o="object"==typeof self&&self&&self.Object===Object&&self,a=r||o||Function("return this")();e.exports=a},function(e,t,n){"use strict";function r(e,t){var n={};for(var r in e)t.indexOf(r)>=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function o(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t"+o+""};e.exports=function(e,t){var n={};n[e]=t(u),r(r.P+r.F*o(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",n)}},function(e,t,n){function r(e){return"function"==typeof e?e:null==e?i:"object"==typeof e?u(e)?a(e[0],e[1]):o(e):s(e)}var o=n(860),a=n(868),i=n(97),u=n(16),s=n(874);e.exports=r},function(e,t,n){"use strict";function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}var u=n(1),s=n.n(u),c=n(6),l=n.n(c),f=n(45),p=n.n(f),d=n(2),h=n.n(d),v=n(12),y=n.n(v),m=function(){function e(e,t){for(var n=0;n0?o(r(e),9007199254740991):0}},function(e,t,n){"use strict";function r(e,t){return 1===e.nodeType&&e.getAttribute(h)===String(t)||8===e.nodeType&&e.nodeValue===" react-text: "+t+" "||8===e.nodeType&&e.nodeValue===" react-empty: "+t+" "}function o(e){for(var t;t=e._renderedComponent;)e=t;return e}function a(e,t){var n=o(e);n._hostNode=t,t[y]=n}function i(e){var t=e._hostNode;t&&(delete t[y],e._hostNode=null)}function u(e,t){if(!(e._flags&v.hasCachedChildNodes)){var n=e._renderedChildren,i=t.firstChild;e:for(var u in n)if(n.hasOwnProperty(u)){var s=n[u],c=o(s)._domID;if(0!==c){for(;null!==i;i=i.nextSibling)if(r(i,c)){a(s,i);continue e}f("32",c)}}e._flags|=v.hasCachedChildNodes}}function s(e){if(e[y])return e[y];for(var t=[];!e[y];){if(t.push(e),!e.parentNode)return null;e=e.parentNode}for(var n,r;e&&(r=e[y]);e=t.pop())n=r,t.length&&u(r,e);return n}function c(e){var t=s(e);return null!=t&&t._hostNode===e?t:null}function l(e){if(void 0===e._hostNode&&f("33"),e._hostNode)return e._hostNode;for(var t=[];!e._hostNode;)t.push(e),e._hostParent||f("34"),e=e._hostParent;for(;t.length;e=t.pop())u(e,e._hostNode);return e._hostNode}var f=n(11),p=n(94),d=n(319),h=(n(4),p.ID_ATTRIBUTE_NAME),v=d,y="__reactInternalInstance$"+Math.random().toString(36).slice(2),m={getClosestInstanceFromNode:s,getInstanceFromNode:c,getNodeFromInstance:l,precacheChildNodes:u,precacheNode:a,uncacheNode:i};e.exports=m},function(e,t,n){"use strict";var r=n(745),o=(n(346),n(748));n.d(t,"a",function(){return r.a}),n.d(t,"b",function(){return o.a})},function(e,t,n){"use strict";var r=function(e,t,n,r,o,a,i,u){if(!e){var s;if(void 0===t)s=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,o,a,i,u],l=0;s=new Error(t.replace(/%s/g,function(){return c[l++]})),s.name="Invariant Violation"}throw s.framesToPop=1,s}};e.exports=r},function(e,t,n){var r=n(52);e.exports=function(e){return Object(r(e))}},function(e,t,n){"use strict";var r=function(){};e.exports=r},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t,n){var r=n(366),o="object"==typeof self&&self&&self.Object===Object&&self,a=r||o||Function("return this")();e.exports=a},function(e,t,n){"use strict";function r(e,t){var n={};for(var r in e)t.indexOf(r)>=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function o(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t"+o+""};e.exports=function(e,t){var n={};n[e]=t(u),r(r.P+r.F*o(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",n)}},function(e,t,n){function r(e){return"function"==typeof e?e:null==e?i:"object"==typeof e?u(e)?a(e[0],e[1]):o(e):s(e)}var o=n(860),a=n(868),i=n(97),u=n(16),s=n(874);e.exports=r},function(e,t,n){"use strict";function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}var u=n(1),s=n.n(u),c=n(6),l=n.n(c),f=n(45),p=n.n(f),d=n(2),h=n.n(d),v=n(12),y=n.n(v),m=function(){function e(e,t){for(var n=0;n0?r:n)(e)}},function(e,t,n){var r=n(0),o=n(50),a=n(8);e.exports=function(e,t){var n=(o.Object||{})[e]||Object[e],i={};i[e]=t(n),r(r.S+r.F*a(function(){n(1)}),"Object",i)}},function(e,t,n){var r=n(46),o=n(112),a=n(25),i=n(21),u=n(206);e.exports=function(e,t){var n=1==e,s=2==e,c=3==e,l=4==e,f=6==e,p=5==e||f,d=t||u;return function(t,u,h){for(var v,y,m=a(t),g=o(m),b=r(u,h,3),w=i(g.length),_=0,x=n?d(t,w):s?d(t,0):void 0;w>_;_++)if((p||_ in g)&&(v=g[_],y=b(v,_,m),e))if(n)x[_]=y;else if(y)switch(e){case 3:return!0;case 5:return v;case 6:return _;case 2:x.push(v)}else if(l)return!1;return f?-1:c||l?l:x}}},function(e,t,n){"use strict";function r(){T.ReactReconcileTransaction&&x||l("123")}function o(){this.reinitializeTransaction(),this.dirtyComponentsLength=null,this.callbackQueue=p.getPooled(),this.reconcileTransaction=T.ReactReconcileTransaction.getPooled(!0)}function a(e,t,n,o,a,i){return r(),x.batchedUpdates(e,t,n,o,a,i)}function i(e,t){return e._mountOrder-t._mountOrder}function u(e){var t=e.dirtyComponentsLength;t!==g.length&&l("124",t,g.length),g.sort(i),b++;for(var n=0;nn;)o[n]=t[n++];return o},Pe=function(e,t,n){q(e,t,{get:function(){return this._d[n]}})},ke=function(e){var t,n,r,o,a,i,u=x(e),s=arguments.length,l=s>1?arguments[1]:void 0,f=void 0!==l,p=I(u);if(void 0!=p&&!E(p)){for(i=p.call(u),r=[],t=0;!(a=i.next()).done;t++)r.push(a.value);u=r}for(f&&s>2&&(l=c(l,arguments[2],2)),t=0,n=v(u.length),o=Ce(this,n);n>t;t++)o[t]=f?l(u[t],t):u[t];return o},Re=function(){for(var e=0,t=arguments.length,n=Ce(this,t);t>e;)n[e]=arguments[e++];return n},je=!!V&&a(function(){de.call(new V(1))}),De=function(){return de.apply(je?fe.call(Oe(this)):Oe(this),arguments)},Ae={copyWithin:function(e,t){return F.call(Oe(this),e,t,arguments.length>2?arguments[2]:void 0)},every:function(e){return Q(Oe(this),e,arguments.length>1?arguments[1]:void 0)},fill:function(e){return L.apply(Oe(this),arguments)},filter:function(e){return Ie(this,X(Oe(this),e,arguments.length>1?arguments[1]:void 0))},find:function(e){return Z(Oe(this),e,arguments.length>1?arguments[1]:void 0)},findIndex:function(e){return ee(Oe(this),e,arguments.length>1?arguments[1]:void 0)},forEach:function(e){$(Oe(this),e,arguments.length>1?arguments[1]:void 0)},indexOf:function(e){return ne(Oe(this),e,arguments.length>1?arguments[1]:void 0)},includes:function(e){return te(Oe(this),e,arguments.length>1?arguments[1]:void 0)},join:function(e){return ce.apply(Oe(this),arguments)},lastIndexOf:function(e){return ie.apply(Oe(this),arguments)},map:function(e){return _e(Oe(this),e,arguments.length>1?arguments[1]:void 0)},reduce:function(e){return ue.apply(Oe(this),arguments)},reduceRight:function(e){return se.apply(Oe(this),arguments)},reverse:function(){for(var e,t=this,n=Oe(t).length,r=Math.floor(n/2),o=0;o1?arguments[1]:void 0)},sort:function(e){return le.call(Oe(this),e)},subarray:function(e,t){var n=Oe(this),r=n.length,o=m(e,r);return new(j(n,n[me]))(n.buffer,n.byteOffset+o*n.BYTES_PER_ELEMENT,v((void 0===t?r:m(t,r))-o))}},Me=function(e,t){return Ie(this,fe.call(Oe(this),e,t))},Ne=function(e){Oe(this);var t=Se(arguments[1],1),n=this.length,r=x(e),o=v(r.length),a=0;if(o+t>n)throw W("Wrong length!");for(;a255?255:255&r),o.v[d](n*t+o.o,r,xe)},P=function(e,t){q(e,t,{get:function(){return I(this,t)},set:function(e){return T(this,t,e)},enumerable:!0})};b?(h=n(function(e,n,r,o){l(e,h,c,"_d");var a,i,u,s,f=0,d=0;if(_(n)){if(!(n instanceof Y||"ArrayBuffer"==(s=w(n))||"SharedArrayBuffer"==s))return be in n?Te(h,n):ke.call(h,n);a=n,d=Se(r,t);var m=n.byteLength;if(void 0===o){if(m%t)throw W("Wrong length!");if((i=m-d)<0)throw W("Wrong length!")}else if((i=v(o)*t)+d>m)throw W("Wrong length!");u=i/t}else u=y(n),i=u*t,a=new Y(i);for(p(e,"_d",{b:a,o:d,l:i,e:u,v:new K(a)});ft}function o(e,t){return r(e)||t.home?"home":(p()(t.threadID,"should be set if home isn't"),t.threadID)}function a(e,t){return r(e)?Object(d.d)():t}function i(e,t){return r(e)?Object(d.e)():t}n.d(t,"c",function(){return g}),n.d(t,"b",function(){return b}),n.d(t,"a",function(){return w}),n.d(t,"d",function(){return _});var u=n(32),s=n(134),c=(n.n(s),n(181)),l=n.n(c),f=n(24),p=n.n(f),d=n(43),h=n(180),v=n(429),y=n(273),m=n(86),g=Object(s.createSelector)(function(e){return e.threadInfos},function(e){return l()("currentUser.subscribed")(e)}),b=(Object(s.createSelector)(function(e){return e.navInfo},function(e){return e.home?"home":(p()(e.threadID,"either home or threadID should be set"),e.threadID)}),Object(s.createSelector)(function(e){return e.navInfo},function(e){return e.threadInfos},g,function(e,t,n){if(e.home)return n?"home":null;p()(e.threadID,"either home or threadID should be set");var r=t[e.threadID];return r&&Object(m.b)(r,u.d.VISIBLE)?e.threadID:null})),w=Object(s.createSelector)(function(e){return e.entryStore.lastUserInteractionCalendar},function(e){return e.navInfo},function(e,t){var n=e;return p()(void 0!==n&&null!==n,"calendar should have a lastUserInteraction entry"),function(){return{navID:o(n,t),startDate:a(n,t.startDate),endDate:i(n,t.endDate)}}}),_=Object(s.createSelector)(function(e){return e.threadInfos},g,b,function(e){return e.navInfo.threadID},function(e,t,n,r){var o=new v.a;r&&!e[r]&&o.addEntry(r,y.c),o.addEntry("home",y.a);for(var a in e){var i=e[a];o.addEntry(a,i.name+" "+i.description)}return o.addEntry("new",y.b),o})},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t){e.exports=!1},function(e,t,n){var r=n(281),o=n(190);e.exports=Object.keys||function(e){return r(e,o)}},function(e,t,n){var r=n(53),o=Math.max,a=Math.min;e.exports=function(e,t){return e=r(e),e<0?o(e+t,0):a(e,t)}},function(e,t,n){var r=n(5),o=n(282),a=n(190),i=n(189)("IE_PROTO"),u=function(){},s=function(){var e,t=n(187)("iframe"),r=a.length;for(t.style.display="none",n(191).appendChild(t),t.src="javascript:",e=t.contentWindow.document,e.open(),e.write("