diff --git a/server/permissions.php b/server/permissions.php index 8b9edaed6..93c57dec0 100644 --- a/server/permissions.php +++ b/server/permissions.php @@ -1,848 +1,971 @@ 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: array< +// permission: string, +// array(value => 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); } +// $roletype: int +// cannot be zero or null! +// $changed_roletype_permissions: +// array +// returns: (null if failed) +// ?array( +// to_save => array bool, source => int), +// >, +// permissions_for_children: +// ?array bool, source => int)> +// roletype: int, +// )>, +// to_delete: array, +// ) +function edit_roletype_permissions($roletype, $changed_roletype_permissions) { + global $conn; + + $roletype = (int)$roletype; + if ($roletype === 0) { + return null; + } + + $query = <<query($query); + $row = $result->fetch_assoc(); + if (!$row) { + return null; + } + $thread_id = (int)$row['thread']; + $vis_rules = (int)$row['visibility_rules']; + $new_roletype_permissions = array_filter(array_merge( + json_decode($row['permissions'], true), + $changed_roletype_permissions + )); + + $encoded_roletype_permissions = + $conn->real_escape_string(json_encode($new_roletype_permissions)); + $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']; + $permissions_from_parent = $row['permissions_from_parent'] + ? json_decode($row['permissions_from_parent'], true) + : null; + $old_permissions = json_decode($row['permissions'], true); + $old_permissions_for_children = $row['permissions_for_children'] + ? json_decode($row['permissions_for_children'], true) + : null; + + $permissions = make_permissions_blob( + $new_roletype_permissions, + $permissions_from_parent, + $thread_id, + $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); +} + // $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/server/scripts/add_permission_types.php b/server/scripts/add_permission_types.php new file mode 100644 index 000000000..62daefcb7 --- /dev/null +++ b/server/scripts/add_permission_types.php @@ -0,0 +1,73 @@ +query($query); + +$parents_to_children = array(); +$children_to_parents = array(); +$threads_to_admin_roletype = array(); +while ($row = $results->fetch_assoc()) { + $roletype = (int)$row['id']; + $thread_id = (int)$row['thread']; + $threads_to_admin_roletype[$thread_id] = $roletype; + + if ($row['parent_thread_id']) { + $parent_thread_id = (int)$row['parent_thread_id']; + $children_to_parents[$thread_id] = $parent_thread_id; + if (!isset($parents_to_children[$parent_thread_id])) { + $parents_to_children[$parent_thread_id] = array(); + } + $parents_to_children[$parent_thread_id][$thread_id] = $thread_id; + } + + if (!isset($parents_to_children[$thread_id])) { + $parents_to_children[$thread_id] = array(); + } +} +echo "Total thread count: " . count($parents_to_children) . "\n"; + +$new_permissions = array( + PERMISSION_REMOVE_MEMBERS => true, + PERMISSION_CHANGE_ROLE => true, + PERMISSION_PREFIX_DESCENDANT . PERMISSION_REMOVE_MEMBERS => true, + PERMISSION_PREFIX_DESCENDANT . PERMISSION_CHANGE_ROLE => true, +); + +while ($parents_to_children) { + // Select just the threads whose children have already been processed + $current_threads = array_keys(array_filter( + $parents_to_children, + function($children) { return !$children; } + )); + echo "This round's leaf nodes: " . print_r($current_threads, true); + + $to_save = array(); + $to_delete = array(); + foreach ($current_threads as $thread_id) { + $admin_roletype = $threads_to_admin_roletype[$thread_id]; + $results = edit_roletype_permissions($admin_roletype, $new_permissions); + $to_save = array_merge($to_save, $results['to_save']); + $to_delete = array_merge($to_delete, $results['to_delete']); + } + save_user_roles($to_save); + delete_user_roles($to_delete); + + $parents_to_children = array_filter($parents_to_children); + foreach ($current_threads as $thread_id) { + if (!isset($children_to_parents[$thread_id])) { + continue; + } + $parent_id = $children_to_parents[$thread_id]; + unset($parents_to_children[$parent_id][$thread_id]); + } +}