def do_get_streams( user_profile: UserProfile, include_public: bool = True, include_web_public: bool = False, include_subscribed: bool = True, include_all_active: bool = False, include_default: bool = False, include_owner_subscribed: bool = False, ) -> List[APIStreamDict]: # This function is only used by API clients now. if include_all_active and not user_profile.is_realm_admin: raise JsonableError(_("User not authorized for this query")) include_public = include_public and user_profile.can_access_public_streams( ) # Start out with all active streams in the realm. query = Stream.objects.filter(realm=user_profile.realm, deactivated=False) if include_all_active: streams = Stream.get_client_data(query) else: # We construct a query as the or (|) of the various sources # this user requested streams from. query_filter: Optional[Q] = None def add_filter_option(option: Q) -> None: nonlocal query_filter if query_filter is None: query_filter = option else: query_filter |= option if include_subscribed: subscribed_stream_ids = get_subscribed_stream_ids_for_user( user_profile) recipient_check = Q(id__in=set(subscribed_stream_ids)) add_filter_option(recipient_check) if include_public: invite_only_check = Q(invite_only=False) add_filter_option(invite_only_check) if include_web_public: # This should match get_web_public_streams_queryset web_public_check = Q( is_web_public=True, invite_only=False, history_public_to_subscribers=True, deactivated=False, ) add_filter_option(web_public_check) if include_owner_subscribed and user_profile.is_bot: bot_owner = user_profile.bot_owner assert bot_owner is not None owner_stream_ids = get_subscribed_stream_ids_for_user(bot_owner) owner_subscribed_check = Q(id__in=set(owner_stream_ids)) add_filter_option(owner_subscribed_check) if query_filter is not None: query = query.filter(query_filter) streams = Stream.get_client_data(query) else: # Don't bother going to the database with no valid sources streams = [] streams.sort(key=lambda elt: elt["name"]) if include_default: is_default = {} default_streams = get_default_streams_for_realm(user_profile.realm_id) for default_stream in default_streams: is_default[default_stream.id] = True for stream in streams: stream["is_default"] = is_default.get(stream["stream_id"], False) return streams
def json_change_settings( request: HttpRequest, user_profile: UserProfile, full_name: str = REQ(default=""), email: str = REQ(default=""), old_password: str = REQ(default=""), new_password: str = REQ(default=""), twenty_four_hour_time: Optional[bool] = REQ(json_validator=check_bool, default=None), dense_mode: Optional[bool] = REQ(json_validator=check_bool, default=None), starred_message_counts: Optional[bool] = REQ(json_validator=check_bool, default=None), fluid_layout_width: Optional[bool] = REQ(json_validator=check_bool, default=None), high_contrast_mode: Optional[bool] = REQ(json_validator=check_bool, default=None), color_scheme: Optional[int] = REQ(json_validator=check_int_in( UserProfile.COLOR_SCHEME_CHOICES), default=None), translate_emoticons: Optional[bool] = REQ(json_validator=check_bool, default=None), default_language: Optional[str] = REQ(default=None), default_view: Optional[str] = REQ( str_validator=check_string_in(default_view_options), default=None), left_side_userlist: Optional[bool] = REQ(json_validator=check_bool, default=None), emojiset: Optional[str] = REQ( str_validator=check_string_in(emojiset_choices), default=None), demote_inactive_streams: Optional[int] = REQ(json_validator=check_int_in( UserProfile.DEMOTE_STREAMS_CHOICES), default=None), timezone: Optional[str] = REQ(str_validator=check_string_in( pytz.all_timezones_set), default=None), email_notifications_batching_period_seconds: Optional[int] = REQ( json_validator=check_int, default=None), enable_stream_desktop_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_stream_email_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_stream_push_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_stream_audible_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), wildcard_mentions_notify: Optional[bool] = REQ(json_validator=check_bool, default=None), notification_sound: Optional[str] = REQ(default=None), enable_desktop_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_sounds: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_offline_email_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_offline_push_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_online_push_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_digest_emails: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_login_emails: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_marketing_emails: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_in_email_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), pm_content_in_desktop_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), desktop_icon_count_display: Optional[int] = REQ(json_validator=check_int, default=None), realm_name_in_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None), enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None), ) -> HttpResponse: # We can't use REQ for this widget because # get_available_language_codes requires provisioning to be # complete. if default_language is not None and default_language not in get_available_language_codes( ): raise JsonableError(_("Invalid default_language")) if (notification_sound is not None and notification_sound not in get_available_notification_sounds() and notification_sound != "none"): raise JsonableError( _("Invalid notification sound '{}'").format(notification_sound)) if email_notifications_batching_period_seconds is not None and ( email_notifications_batching_period_seconds <= 0 or email_notifications_batching_period_seconds > 7 * 24 * 60 * 60): # We set a limit of one week for the batching period raise JsonableError( _("Invalid email batching period: {} seconds").format( email_notifications_batching_period_seconds)) if new_password != "": return_data: Dict[str, Any] = {} if email_belongs_to_ldap(user_profile.realm, user_profile.delivery_email): raise JsonableError(_("Your Zulip password is managed in LDAP")) try: if not authenticate( request, username=user_profile.delivery_email, password=old_password, realm=user_profile.realm, return_data=return_data, ): raise JsonableError(_("Wrong password!")) except RateLimited as e: assert e.secs_to_freedom is not None secs_to_freedom = int(e.secs_to_freedom) raise JsonableError( _("You're making too many attempts! Try again in {} seconds."). format(secs_to_freedom), ) if not check_password_strength(new_password): raise JsonableError(_("New password is too weak!")) do_change_password(user_profile, new_password) # In Django 1.10, password changes invalidates sessions, see # https://docs.djangoproject.com/en/1.10/topics/auth/default/#session-invalidation-on-password-change # for details. To avoid this logging the user out of their own # session (which would provide a confusing UX at best), we # update the session hash here. update_session_auth_hash(request, user_profile) # We also save the session to the DB immediately to mitigate # race conditions. In theory, there is still a race condition # and to completely avoid it we will have to use some kind of # mutex lock in `django.contrib.auth.get_user` where session # is verified. To make that lock work we will have to control # the AuthenticationMiddleware which is currently controlled # by Django, request.session.save() result: Dict[str, Any] = {} new_email = email.strip() if user_profile.delivery_email != new_email and new_email != "": if user_profile.realm.email_changes_disabled and not user_profile.is_realm_admin: raise JsonableError( _("Email address changes are disabled in this organization.")) error = validate_email_is_valid( new_email, get_realm_email_validator(user_profile.realm), ) if error: raise JsonableError(error) try: validate_email_not_already_in_realm( user_profile.realm, new_email, verbose=False, ) except ValidationError as e: raise JsonableError(e.message) do_start_email_change_process(user_profile, new_email) if user_profile.full_name != full_name and full_name.strip() != "": if name_changes_disabled( user_profile.realm) and not user_profile.is_realm_admin: # Failingly silently is fine -- they can't do it through the UI, so # they'd have to be trying to break the rules. pass else: # Note that check_change_full_name strips the passed name automatically check_change_full_name(user_profile, full_name, user_profile) # Loop over user_profile.property_types request_settings = { k: v for k, v in list(locals().items()) if k in user_profile.property_types } for k, v in list(request_settings.items()): if v is not None and getattr(user_profile, k) != v: do_set_user_display_setting(user_profile, k, v) req_vars = { k: v for k, v in list(locals().items()) if k in user_profile.notification_setting_types } for k, v in list(req_vars.items()): if v is not None and getattr(user_profile, k) != v: do_change_notification_settings(user_profile, k, v, acting_user=user_profile) if timezone is not None and user_profile.timezone != timezone: do_set_user_display_setting(user_profile, "timezone", timezone) # TODO: Do this more generally. from zerver.lib.request import get_request_notes request_notes = get_request_notes(request) for req_var in request.POST: if req_var not in request_notes.processed_parameters: request_notes.ignored_parameters.add(req_var) if len(request_notes.ignored_parameters) > 0: result["ignored_parameters_unsupported"] = list( request_notes.ignored_parameters) return json_success(result)
def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse: if not user_profile.is_staff: raise JsonableError(_("Must be an server administrator")) return view_func(request, user_profile, *args, **kwargs)
def send_to_push_bouncer( method: str, endpoint: str, post_data: Union[bytes, Mapping[str, Union[str, int, None, bytes]]], extra_headers: Mapping[str, str] = {}, ) -> Dict[str, object]: """While it does actually send the notice, this function has a lot of code and comments around error handling for the push notifications bouncer. There are several classes of failures, each with its own potential solution: * Network errors with requests.request. We raise an exception to signal it to the callers. * 500 errors from the push bouncer or other unexpected responses; we don't try to parse the response, but do make clear the cause. * 400 errors from the push bouncer. Here there are 2 categories: Our server failed to connect to the push bouncer (should throw) vs. client-side errors like an invalid token. """ url = urllib.parse.urljoin(settings.PUSH_NOTIFICATION_BOUNCER_URL, "/api/v1/remotes/" + endpoint) api_auth = requests.auth.HTTPBasicAuth(settings.ZULIP_ORG_ID, settings.ZULIP_ORG_KEY) headers = {"User-agent": f"ZulipServer/{ZULIP_VERSION}"} headers.update(extra_headers) try: res = requests.request(method, url, data=post_data, auth=api_auth, timeout=30, verify=True, headers=headers) except ( requests.exceptions.Timeout, requests.exceptions.SSLError, requests.exceptions.ConnectionError, ) as e: raise PushNotificationBouncerRetryLaterError( f"{e.__class__.__name__} while trying to connect to push notification bouncer" ) if res.status_code >= 500: # 500s should be resolved by the people who run the push # notification bouncer service, and they'll get an appropriate # error notification from the server. We raise an exception to signal # to the callers that the attempt failed and they can retry. error_msg = "Received 500 from push notification bouncer" logging.warning(error_msg) raise PushNotificationBouncerRetryLaterError(error_msg) elif res.status_code >= 400: # If JSON parsing errors, just let that exception happen result_dict = orjson.loads(res.content) msg = result_dict["msg"] if "code" in result_dict and result_dict[ "code"] == "INVALID_ZULIP_SERVER": # Invalid Zulip server credentials should email this server's admins raise PushNotificationBouncerException( _("Push notifications bouncer error: {}").format(msg)) else: # But most other errors coming from the push bouncer # server are client errors (e.g. never-registered token) # and should be handled as such. raise JsonableError(msg) elif res.status_code != 200: # Anything else is unexpected and likely suggests a bug in # this version of Zulip, so we throw an exception that will # email the server admins. raise PushNotificationBouncerException( f"Push notification bouncer returned unexpected status code {res.status_code}" ) # If we don't throw an exception, it's a successful bounce! return orjson.loads(res.content)
def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name: str = REQ(), min_length: Optional[int] = REQ( converter=to_non_negative_int, default=None), start: Optional[datetime] = REQ(converter=to_utc_datetime, default=None), end: Optional[datetime] = REQ(converter=to_utc_datetime, default=None), realm: Optional[Realm] = None, for_installation: bool = False) -> HttpResponse: aggregate_table = RealmCount if for_installation: aggregate_table = InstallationCount if chart_name == 'number_of_humans': stats = [ COUNT_STATS['1day_actives::day'], COUNT_STATS['realm_active_humans::day'], COUNT_STATS['active_users_audit:is_bot:day'] ] tables = [aggregate_table] subgroup_to_label = { stats[0]: { None: '_1day' }, stats[1]: { None: '_15day' }, stats[2]: { 'false': 'all_time' } } # type: Dict[CountStat, Dict[Optional[str], str]] labels_sort_function = None include_empty_subgroups = True elif chart_name == 'messages_sent_over_time': stats = [COUNT_STATS['messages_sent:is_bot:hour']] tables = [aggregate_table, UserCount] subgroup_to_label = {stats[0]: {'false': 'human', 'true': 'bot'}} labels_sort_function = None include_empty_subgroups = True elif chart_name == 'messages_sent_by_message_type': stats = [COUNT_STATS['messages_sent:message_type:day']] tables = [aggregate_table, UserCount] subgroup_to_label = { stats[0]: { 'public_stream': _('Public streams'), 'private_stream': _('Private streams'), 'private_message': _('Private messages'), 'huddle_message': _('Group private messages') } } labels_sort_function = lambda data: sort_by_totals(data['everyone']) include_empty_subgroups = True elif chart_name == 'messages_sent_by_client': stats = [COUNT_STATS['messages_sent:client:day']] tables = [aggregate_table, UserCount] # Note that the labels are further re-written by client_label_map subgroup_to_label = { stats[0]: { str(id): name for id, name in Client.objects.values_list('id', 'name') } } labels_sort_function = sort_client_labels include_empty_subgroups = False else: raise JsonableError(_("Unknown chart name: %s") % (chart_name, )) # Most likely someone using our API endpoint. The /stats page does not # pass a start or end in its requests. if start is not None: start = convert_to_UTC(start) if end is not None: end = convert_to_UTC(end) if start is not None and end is not None and start > end: raise JsonableError( _("Start time is later than end time. Start: %(start)s, End: %(end)s" ) % { 'start': start, 'end': end }) if realm is None: realm = user_profile.realm if start is None: if for_installation: start = installation_epoch() else: start = realm.date_created if end is None: end = max( last_successful_fill(stat.property) or datetime.min.replace( tzinfo=timezone_utc) for stat in stats) if end is None or start > end: logging.warning( "User from realm %s attempted to access /stats, but the computed " "start time: %s (creation of realm or installation) is later than the computed " "end time: %s (last successful analytics update). Is the " "analytics cron job running?" % (realm.string_id, start, end)) raise JsonableError( _("No analytics data available. Please contact your server administrator." )) assert len(set([stat.frequency for stat in stats])) == 1 end_times = time_range(start, end, stats[0].frequency, min_length) data = { 'end_times': end_times, 'frequency': stats[0].frequency } # type: Dict[str, Any] aggregation_level = { InstallationCount: 'everyone', RealmCount: 'everyone', UserCount: 'user' } # -1 is a placeholder value, since there is no relevant filtering on InstallationCount id_value = { InstallationCount: -1, RealmCount: realm.id, UserCount: user_profile.id } for table in tables: data[aggregation_level[table]] = {} for stat in stats: data[aggregation_level[table]].update( get_time_series_by_subgroup(stat, table, id_value[table], end_times, subgroup_to_label[stat], include_empty_subgroups)) if labels_sort_function is not None: data['display_order'] = labels_sort_function(data) else: data['display_order'] = None return json_success(data=data)
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse: if request.user.is_bot: raise JsonableError(_("This endpoint does not accept bot requests.")) return view_func(request, *args, **kwargs)
def update_plan( request: HttpRequest, user: UserProfile, status: Optional[int] = REQ( "status", json_validator=check_int_in([ CustomerPlan.ACTIVE, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE, CustomerPlan.ENDED, ]), default=None, ), licenses: Optional[int] = REQ("licenses", json_validator=check_int, default=None), licenses_at_next_renewal: Optional[int] = REQ("licenses_at_next_renewal", json_validator=check_int, default=None), ) -> HttpResponse: plan = get_current_plan_by_realm(user.realm) assert plan is not None # for mypy new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed( plan, timezone_now()) if new_plan is not None: raise JsonableError( _("Unable to update the plan. The plan has been expired and replaced with a new plan." )) if last_ledger_entry is None: raise JsonableError( _("Unable to update the plan. The plan has ended.")) if status is not None: if status == CustomerPlan.ACTIVE: assert plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE do_change_plan_status(plan, status) elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: assert plan.status == CustomerPlan.ACTIVE downgrade_at_the_end_of_billing_cycle(user.realm) elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE: assert plan.billing_schedule == CustomerPlan.MONTHLY assert plan.status == CustomerPlan.ACTIVE assert plan.fixed_price is None do_change_plan_status(plan, status) elif status == CustomerPlan.ENDED: assert plan.is_free_trial() downgrade_now_without_creating_additional_invoices(user.realm) return json_success(request) if licenses is not None: if plan.automanage_licenses: raise JsonableError( _("Unable to update licenses manually. Your plan is on automatic license management." )) if last_ledger_entry.licenses == licenses: raise JsonableError( _("Your plan is already on {licenses} licenses in the current billing period." ).format(licenses=licenses)) if last_ledger_entry.licenses > licenses: raise JsonableError( _("You cannot decrease the licenses in the current billing period." ).format(licenses=licenses)) validate_licenses(plan.charge_automatically, licenses, get_latest_seat_count(user.realm)) update_license_ledger_for_manual_plan(plan, timezone_now(), licenses=licenses) return json_success(request) if licenses_at_next_renewal is not None: if plan.automanage_licenses: raise JsonableError( _("Unable to update licenses manually. Your plan is on automatic license management." )) if last_ledger_entry.licenses_at_next_renewal == licenses_at_next_renewal: raise JsonableError( _("Your plan is already scheduled to renew with {licenses_at_next_renewal} licenses." ).format(licenses_at_next_renewal=licenses_at_next_renewal)) validate_licenses( plan.charge_automatically, licenses_at_next_renewal, get_latest_seat_count(user.realm), ) update_license_ledger_for_manual_plan( plan, timezone_now(), licenses_at_next_renewal=licenses_at_next_renewal) return json_success(request) raise JsonableError(_("Nothing to change."))
def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name: Text=REQ(), min_length: Optional[int]=REQ(converter=to_non_negative_int, default=None), start: Optional[datetime]=REQ(converter=to_utc_datetime, default=None), end: Optional[datetime]=REQ(converter=to_utc_datetime, default=None)) -> HttpResponse: if chart_name == 'number_of_humans': stat = COUNT_STATS['realm_active_humans::day'] tables = [RealmCount] subgroup_to_label = {None: 'human'} # type: Dict[Optional[str], str] labels_sort_function = None include_empty_subgroups = True elif chart_name == 'messages_sent_over_time': stat = COUNT_STATS['messages_sent:is_bot:hour'] tables = [RealmCount, UserCount] subgroup_to_label = {'false': 'human', 'true': 'bot'} labels_sort_function = None include_empty_subgroups = True elif chart_name == 'messages_sent_by_message_type': stat = COUNT_STATS['messages_sent:message_type:day'] tables = [RealmCount, UserCount] subgroup_to_label = {'public_stream': 'Public streams', 'private_stream': 'Private streams', 'private_message': 'Private messages', 'huddle_message': 'Group private messages'} labels_sort_function = lambda data: sort_by_totals(data['realm']) include_empty_subgroups = True elif chart_name == 'messages_sent_by_client': stat = COUNT_STATS['messages_sent:client:day'] tables = [RealmCount, UserCount] # Note that the labels are further re-written by client_label_map subgroup_to_label = {str(id): name for id, name in Client.objects.values_list('id', 'name')} labels_sort_function = sort_client_labels include_empty_subgroups = False else: raise JsonableError(_("Unknown chart name: %s") % (chart_name,)) # Most likely someone using our API endpoint. The /stats page does not # pass a start or end in its requests. if start is not None: start = convert_to_UTC(start) if end is not None: end = convert_to_UTC(end) if start is not None and end is not None and start > end: raise JsonableError(_("Start time is later than end time. Start: %(start)s, End: %(end)s") % {'start': start, 'end': end}) realm = user_profile.realm if start is None: start = realm.date_created if end is None: end = last_successful_fill(stat.property) if end is None or start > end: logging.warning("User from realm %s attempted to access /stats, but the computed " "start time: %s (creation time of realm) is later than the computed " "end time: %s (last successful analytics update). Is the " "analytics cron job running?" % (realm.string_id, start, end)) raise JsonableError(_("No analytics data available. Please contact your server administrator.")) end_times = time_range(start, end, stat.frequency, min_length) data = {'end_times': end_times, 'frequency': stat.frequency} for table in tables: if table == RealmCount: data['realm'] = get_time_series_by_subgroup( stat, RealmCount, realm.id, end_times, subgroup_to_label, include_empty_subgroups) if table == UserCount: data['user'] = get_time_series_by_subgroup( stat, UserCount, user_profile.id, end_times, subgroup_to_label, include_empty_subgroups) if labels_sort_function is not None: data['display_order'] = labels_sort_function(data) else: data['display_order'] = None return json_success(data=data)
def update_stream_backend( request: HttpRequest, user_profile: UserProfile, stream_id: int, description: Optional[str] = REQ(str_validator=check_capped_string( Stream.MAX_DESCRIPTION_LENGTH), default=None), is_private: Optional[bool] = REQ(json_validator=check_bool, default=None), is_announcement_only: Optional[bool] = REQ(json_validator=check_bool, default=None), stream_post_policy: Optional[int] = REQ(json_validator=check_int_in( Stream.STREAM_POST_POLICY_TYPES), default=None), history_public_to_subscribers: Optional[bool] = REQ( json_validator=check_bool, default=None), is_web_public: Optional[bool] = REQ(json_validator=check_bool, default=None), new_name: Optional[str] = REQ(default=None), message_retention_days: Optional[Union[int, str]] = REQ( json_validator=check_string_or_int, default=None), ) -> HttpResponse: # We allow realm administrators to to update the stream name and # description even for private streams. (stream, sub) = access_stream_for_delete_or_update(user_profile, stream_id) if message_retention_days is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() user_profile.realm.ensure_not_on_limited_plan() message_retention_days_value = parse_message_retention_days( message_retention_days, Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP) do_change_stream_message_retention_days(stream, message_retention_days_value) if description is not None: if "\n" in description: # We don't allow newline characters in stream descriptions. description = description.replace("\n", " ") do_change_stream_description(stream, description) if new_name is not None: new_name = new_name.strip() if stream.name == new_name: raise JsonableError(_("Stream already has that name!")) if stream.name.lower() != new_name.lower(): # Check that the stream name is available (unless we are # are only changing the casing of the stream name). check_stream_name_available(user_profile.realm, new_name) do_rename_stream(stream, new_name, user_profile) if is_announcement_only is not None: # is_announcement_only is a legacy way to specify # stream_post_policy. We can probably just delete this code, # since we're not aware of clients that used it, but we're # keeping it for backwards-compatibility for now. stream_post_policy = Stream.STREAM_POST_POLICY_EVERYONE if is_announcement_only: stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS if stream_post_policy is not None: do_change_stream_post_policy(stream, stream_post_policy) # But we require even realm administrators to be actually # subscribed to make a private stream public. if is_private is not None: default_stream_ids = { s.id for s in get_default_streams_for_realm(stream.realm_id) } (stream, sub) = access_stream_by_id(user_profile, stream_id) if is_private and stream.id in default_stream_ids: raise JsonableError(_("Default streams cannot be made private.")) if is_web_public: # Enforce restrictions on creating web-public streams. if not user_profile.realm.web_public_streams_enabled(): raise JsonableError(_("Web public streams are not enabled.")) if not user_profile.can_create_web_public_streams(): raise JsonableError(_("Insufficient permission")) # Forbid parameter combinations that are inconsistent if is_private or history_public_to_subscribers is False: raise JsonableError(_("Invalid parameters")) if is_private is not None or is_web_public is not None: do_change_stream_permission(stream, is_private, history_public_to_subscribers, is_web_public) return json_success()
def process_zcommands(content: str, user_profile: UserProfile) -> Dict[str, Any]: def change_mode_setting(setting_name: str, switch_command: str, setting: str, setting_value: int) -> str: msg = ("Changed to {setting_name}! To revert " "{setting_name}, type `/{switch_command}`.".format( setting_name=setting_name, switch_command=switch_command, )) do_change_user_setting( user_profile=user_profile, setting_name=setting, setting_value=setting_value, acting_user=user_profile, ) return msg if not content.startswith("/"): raise JsonableError( _("There should be a leading slash in the zcommand.")) command = content[1:] if command == "ping": return {} elif command == "night": if user_profile.color_scheme == UserProfile.COLOR_SCHEME_NIGHT: return dict(msg="You are still in dark theme.") return dict(msg=change_mode_setting( setting_name="dark theme", switch_command="light", setting="color_scheme", setting_value=UserProfile.COLOR_SCHEME_NIGHT, )) elif command == "day": if user_profile.color_scheme == UserProfile.COLOR_SCHEME_LIGHT: return dict(msg="You are still in light theme.") return dict(msg=change_mode_setting( setting_name="light theme", switch_command="dark", setting="color_scheme", setting_value=UserProfile.COLOR_SCHEME_LIGHT, )) elif command == "fluid-width": if user_profile.fluid_layout_width: return dict(msg="You are still in fluid width mode.") return dict(msg=change_mode_setting( setting_name="fluid-width mode", switch_command="fixed-width", setting="fluid_layout_width", setting_value=True, )) elif command == "fixed-width": if not user_profile.fluid_layout_width: return dict(msg="You are still in fixed width mode.") return dict(msg=change_mode_setting( setting_name="fixed-width mode", switch_command="fluid-width", setting="fluid_layout_width", setting_value=False, )) raise JsonableError(_("No such command: {}").format(command))
def update_realm( request: HttpRequest, user_profile: UserProfile, name: Optional[str] = REQ( str_validator=check_capped_string(Realm.MAX_REALM_NAME_LENGTH), default=None ), description: Optional[str] = REQ( str_validator=check_capped_string(Realm.MAX_REALM_DESCRIPTION_LENGTH), default=None ), emails_restricted_to_domains: Optional[bool] = REQ(json_validator=check_bool, default=None), disallow_disposable_email_addresses: Optional[bool] = REQ( json_validator=check_bool, default=None ), invite_required: Optional[bool] = REQ(json_validator=check_bool, default=None), invite_to_realm_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None ), name_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), email_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), avatar_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), inline_image_preview: Optional[bool] = REQ(json_validator=check_bool, default=None), inline_url_embed_preview: Optional[bool] = REQ(json_validator=check_bool, default=None), add_emoji_by_admins_only: Optional[bool] = REQ(json_validator=check_bool, default=None), allow_message_deleting: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_delete_limit_seconds: Optional[int] = REQ( converter=to_non_negative_int, default=None ), allow_message_editing: Optional[bool] = REQ(json_validator=check_bool, default=None), edit_topic_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_MESSAGE_POLICY_TYPES), default=None ), mandatory_topics: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_edit_limit_seconds: Optional[int] = REQ( converter=to_non_negative_int, default=None ), allow_edit_history: Optional[bool] = REQ(json_validator=check_bool, default=None), default_language: Optional[str] = REQ(default=None), waiting_period_threshold: Optional[int] = REQ(converter=to_non_negative_int, default=None), authentication_methods: Optional[Dict[str, Any]] = REQ( json_validator=check_dict([]), default=None ), notifications_stream_id: Optional[int] = REQ(json_validator=check_int, default=None), signup_notifications_stream_id: Optional[int] = REQ(json_validator=check_int, default=None), message_retention_days_raw: Optional[Union[int, str]] = REQ( "message_retention_days", json_validator=check_string_or_int, default=None ), send_welcome_emails: Optional[bool] = REQ(json_validator=check_bool, default=None), digest_emails_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_allowed_in_email_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None ), bot_creation_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.BOT_CREATION_POLICY_TYPES), default=None ), create_stream_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None ), invite_to_stream_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None ), move_messages_between_streams_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None ), user_group_edit_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None ), private_message_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.PRIVATE_MESSAGE_POLICY_TYPES), default=None ), wildcard_mention_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.WILDCARD_MENTION_POLICY_TYPES), default=None ), email_address_visibility: Optional[int] = REQ( json_validator=check_int_in(Realm.EMAIL_ADDRESS_VISIBILITY_TYPES), default=None ), default_twenty_four_hour_time: Optional[bool] = REQ(json_validator=check_bool, default=None), video_chat_provider: Optional[int] = REQ(json_validator=check_int, default=None), giphy_rating: Optional[int] = REQ(json_validator=check_int, default=None), default_code_block_language: Optional[str] = REQ(default=None), digest_weekday: Optional[int] = REQ( json_validator=check_int_in(Realm.DIGEST_WEEKDAY_VALUES), default=None ), ) -> HttpResponse: realm = user_profile.realm # Additional validation/error checking beyond types go here, so # the entire request can succeed or fail atomically. if default_language is not None and default_language not in get_available_language_codes(): raise JsonableError(_("Invalid language '{}'").format(default_language)) if authentication_methods is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() if True not in list(authentication_methods.values()): raise JsonableError(_("At least one authentication method must be enabled.")) if video_chat_provider is not None and video_chat_provider not in { p["id"] for p in Realm.VIDEO_CHAT_PROVIDERS.values() }: raise JsonableError(_("Invalid video_chat_provider {}").format(video_chat_provider)) if giphy_rating is not None and giphy_rating not in { p["id"] for p in Realm.GIPHY_RATING_OPTIONS.values() }: raise JsonableError(_("Invalid giphy_rating {}").format(giphy_rating)) message_retention_days: Optional[int] = None if message_retention_days_raw is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() realm.ensure_not_on_limited_plan() message_retention_days = parse_message_retention_days( message_retention_days_raw, Realm.MESSAGE_RETENTION_SPECIAL_VALUES_MAP ) # The user of `locals()` here is a bit of a code smell, but it's # restricted to the elements present in realm.property_types. # # TODO: It should be possible to deduplicate this function up # further by some more advanced usage of the # `REQ/has_request_variables` extraction. req_vars = {k: v for k, v in list(locals().items()) if k in realm.property_types} data: Dict[str, Any] = {} for k, v in list(req_vars.items()): if v is not None and getattr(realm, k) != v: do_set_realm_property(realm, k, v, acting_user=user_profile) if isinstance(v, str): data[k] = "updated" else: data[k] = v # The following realm properties do not fit the pattern above # authentication_methods is not supported by the do_set_realm_property # framework because of its bitfield. if authentication_methods is not None and ( realm.authentication_methods_dict() != authentication_methods ): do_set_realm_authentication_methods(realm, authentication_methods, acting_user=user_profile) data["authentication_methods"] = authentication_methods # The message_editing settings are coupled to each other, and thus don't fit # into the do_set_realm_property framework. if ( (allow_message_editing is not None and realm.allow_message_editing != allow_message_editing) or ( message_content_edit_limit_seconds is not None and realm.message_content_edit_limit_seconds != message_content_edit_limit_seconds ) or (edit_topic_policy is not None and realm.edit_topic_policy != edit_topic_policy) ): if allow_message_editing is None: allow_message_editing = realm.allow_message_editing if message_content_edit_limit_seconds is None: message_content_edit_limit_seconds = realm.message_content_edit_limit_seconds if edit_topic_policy is None: edit_topic_policy = realm.edit_topic_policy do_set_realm_message_editing( realm, allow_message_editing, message_content_edit_limit_seconds, edit_topic_policy, acting_user=user_profile, ) data["allow_message_editing"] = allow_message_editing data["message_content_edit_limit_seconds"] = message_content_edit_limit_seconds data["edit_topic_policy"] = edit_topic_policy # Realm.notifications_stream and Realm.signup_notifications_stream are not boolean, # str or integer field, and thus doesn't fit into the do_set_realm_property framework. if notifications_stream_id is not None: if realm.notifications_stream is None or ( realm.notifications_stream.id != notifications_stream_id ): new_notifications_stream = None if notifications_stream_id >= 0: (new_notifications_stream, sub) = access_stream_by_id( user_profile, notifications_stream_id ) do_set_realm_notifications_stream( realm, new_notifications_stream, notifications_stream_id, acting_user=user_profile ) data["notifications_stream_id"] = notifications_stream_id if signup_notifications_stream_id is not None: if realm.signup_notifications_stream is None or ( realm.signup_notifications_stream.id != signup_notifications_stream_id ): new_signup_notifications_stream = None if signup_notifications_stream_id >= 0: (new_signup_notifications_stream, sub) = access_stream_by_id( user_profile, signup_notifications_stream_id ) do_set_realm_signup_notifications_stream( realm, new_signup_notifications_stream, signup_notifications_stream_id, acting_user=user_profile, ) data["signup_notifications_stream_id"] = signup_notifications_stream_id if default_code_block_language is not None: # Migrate '', used in the API to encode the default/None behavior of this feature. if default_code_block_language == "": data["default_code_block_language"] = None else: data["default_code_block_language"] = default_code_block_language return json_success(data)
def list_to_streams( streams_raw: Collection[StreamDict], user_profile: UserProfile, autocreate: bool = False, admin_access_required: bool = False, ) -> Tuple[List[Stream], List[Stream]]: """Converts list of dicts to a list of Streams, validating input in the process For each stream name, we validate it to ensure it meets our requirements for a proper stream name using check_stream_name. This function in autocreate mode should be atomic: either an exception will be raised during a precheck, or all the streams specified will have been created if applicable. @param streams_raw The list of stream dictionaries to process; names should already be stripped of whitespace by the caller. @param user_profile The user for whom we are retrieving the streams @param autocreate Whether we should create streams if they don't already exist """ # Validate all streams, getting extant ones, then get-or-creating the rest. stream_set = {stream_dict["name"] for stream_dict in streams_raw} for stream_name in stream_set: # Stream names should already have been stripped by the # caller, but it makes sense to verify anyway. assert stream_name == stream_name.strip() check_stream_name(stream_name) existing_streams: List[Stream] = [] missing_stream_dicts: List[StreamDict] = [] existing_stream_map = bulk_get_streams(user_profile.realm, stream_set) if admin_access_required: existing_recipient_ids = [ stream.recipient_id for stream in existing_stream_map.values() ] subs = Subscription.objects.filter( user_profile=user_profile, recipient_id__in=existing_recipient_ids, active=True) sub_map = {sub.recipient_id: sub for sub in subs} for stream in existing_stream_map.values(): sub = sub_map.get(stream.recipient_id, None) check_stream_access_for_delete_or_update(user_profile, stream, sub) message_retention_days_not_none = False web_public_stream_requested = False for stream_dict in streams_raw: stream_name = stream_dict["name"] stream = existing_stream_map.get(stream_name.lower()) if stream is None: if stream_dict.get("message_retention_days", None) is not None: message_retention_days_not_none = True missing_stream_dicts.append(stream_dict) if autocreate and stream_dict["is_web_public"]: web_public_stream_requested = True else: existing_streams.append(stream) if len(missing_stream_dicts) == 0: # This is the happy path for callers who expected all of these # streams to exist already. created_streams: List[Stream] = [] else: # autocreate=True path starts here for stream_dict in missing_stream_dicts: invite_only = stream_dict.get("invite_only", False) if invite_only and not user_profile.can_create_private_streams(): raise JsonableError(_("Insufficient permission")) if not invite_only and not user_profile.can_create_public_streams( ): raise JsonableError(_("Insufficient permission")) if not autocreate: raise JsonableError( _("Stream(s) ({}) do not exist").format( ", ".join(stream_dict["name"] for stream_dict in missing_stream_dicts), )) if web_public_stream_requested: if not user_profile.realm.web_public_streams_enabled(): raise JsonableError(_("Web public streams are not enabled.")) if not user_profile.can_create_web_public_streams(): # We set create_web_public_stream_policy to allow only organization owners # to create web-public streams, because of their sensitive nature. raise JsonableError(_("Insufficient permission")) if message_retention_days_not_none: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() user_profile.realm.ensure_not_on_limited_plan() # We already filtered out existing streams, so dup_streams # will normally be an empty list below, but we protect against somebody # else racing to create the same stream. (This is not an entirely # paranoid approach, since often on Zulip two people will discuss # creating a new stream, and both people eagerly do it.) created_streams, dup_streams = create_streams_if_needed( realm=user_profile.realm, stream_dicts=missing_stream_dicts, acting_user=user_profile) existing_streams += dup_streams return existing_streams, created_streams
def check_update_message( user_profile: UserProfile, message_id: int, stream_id: Optional[int] = None, topic_name: Optional[str] = None, propagate_mode: str = "change_one", send_notification_to_old_thread: bool = True, send_notification_to_new_thread: bool = True, content: Optional[str] = None, ) -> int: """This will update a message given the message id and user profile. It checks whether the user profile has the permission to edit the message and raises a JsonableError if otherwise. It returns the number changed. """ message, ignored_user_message = access_message(user_profile, message_id) if not user_profile.realm.allow_message_editing: raise JsonableError( _("Your organization has turned off message editing")) # The zerver/views/message_edit.py call point already strips this # via REQ_topic; so we can delete this line if we arrange a # contract where future callers in the embedded bots system strip # use REQ_topic as well (or otherwise are guaranteed to strip input). if topic_name is not None: topic_name = topic_name.strip() if topic_name == message.topic_name(): topic_name = None validate_message_edit_payload(message, stream_id, topic_name, propagate_mode, content) is_no_topic_msg = message.topic_name() == "(no topic)" if content is not None or topic_name is not None: if not can_edit_content_or_topic(message, user_profile, is_no_topic_msg, content, topic_name): raise JsonableError( _("You don't have permission to edit this message")) # If there is a change to the content, check that it hasn't been too long # Allow an extra 20 seconds since we potentially allow editing 15 seconds # past the limit, and in case there are network issues, etc. The 15 comes # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if # you change this value also change those two parameters in message_edit.js. edit_limit_buffer = 20 if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0: deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer if (timezone_now() - message.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has passed")) # If there is a change to the topic, check that the user is allowed to # edit it and that it has not been too long. If this is not the user who # sent the message, they are not the admin, and the time limit for editing # topics is passed, raise an error. if (topic_name is not None and message.sender != user_profile and not user_profile.is_realm_admin and not user_profile.is_moderator and not is_no_topic_msg): deadline_seconds = Realm.DEFAULT_COMMUNITY_TOPIC_EDITING_LIMIT_SECONDS + edit_limit_buffer if (timezone_now() - message.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message's topic has passed") ) rendering_result = None links_for_embed: Set[str] = set() prior_mention_user_ids: Set[int] = set() mention_data: Optional[MentionData] = None if content is not None: if content.rstrip() == "": content = "(deleted)" content = normalize_body(content) mention_backend = MentionBackend(user_profile.realm_id) mention_data = MentionData( mention_backend=mention_backend, content=content, ) prior_mention_user_ids = get_mentions_for_message_updates(message.id) # We render the message using the current user's realm; since # the cross-realm bots never edit messages, this should be # always correct. # Note: If rendering fails, the called code will raise a JsonableError. rendering_result = render_incoming_message( message, content, user_profile.realm, mention_data=mention_data, ) links_for_embed |= rendering_result.links_for_preview if message.is_stream_message() and rendering_result.mentions_wildcard: stream = access_stream_by_id(user_profile, message.recipient.type_id)[0] if not wildcard_mention_allowed(message.sender, stream): raise JsonableError( _("You do not have permission to use wildcard mentions in this stream." )) new_stream = None number_changed = 0 if stream_id is not None: assert message.is_stream_message() if not user_profile.can_move_messages_between_streams(): raise JsonableError( _("You don't have permission to move this message")) try: access_stream_by_id(user_profile, message.recipient.type_id) except JsonableError: raise JsonableError( _("You don't have permission to move this message due to missing access to its stream" )) new_stream = access_stream_by_id(user_profile, stream_id, require_active=True)[0] check_stream_access_based_on_stream_post_policy( user_profile, new_stream) number_changed = do_update_message( user_profile, message, new_stream, topic_name, propagate_mode, send_notification_to_old_thread, send_notification_to_new_thread, content, rendering_result, prior_mention_user_ids, mention_data, ) if links_for_embed: event_data = { "message_id": message.id, "message_content": message.content, # The choice of `user_profile.realm_id` rather than # `sender.realm_id` must match the decision made in the # `render_incoming_message` call earlier in this function. "message_realm_id": user_profile.realm_id, "urls": list(links_for_embed), } queue_json_publish("embed_links", event_data) return number_changed
def validate_bouncer_token_request(entity: Union[UserProfile, RemoteZulipServer], token: bytes, kind: int) -> None: if kind not in [RemotePushDeviceToken.APNS, RemotePushDeviceToken.GCM]: raise JsonableError(err_("Invalid token type")) validate_entity(entity) validate_token(token, kind)
def check_valid_emoji_name(emoji_name: str) -> None: if emoji_name: if re.match(r"^[0-9a-z.\-_]+(?<![.\-_])$", emoji_name): return raise JsonableError(_("Invalid characters in emoji name")) raise JsonableError(_("Emoji name is missing"))
def add_subscriptions_backend( request: HttpRequest, user_profile: UserProfile, streams_raw: Sequence[Mapping[str, str]] = REQ( "subscriptions", json_validator=add_subscriptions_schema), invite_only: bool = REQ(json_validator=check_bool, default=False), is_web_public: bool = REQ(json_validator=check_bool, default=False), stream_post_policy: int = REQ( json_validator=check_int_in(Stream.STREAM_POST_POLICY_TYPES), default=Stream.STREAM_POST_POLICY_EVERYONE, ), history_public_to_subscribers: Optional[bool] = REQ( json_validator=check_bool, default=None), message_retention_days: Union[str, int] = REQ( json_validator=check_string_or_int, default=RETENTION_DEFAULT), announce: bool = REQ(json_validator=check_bool, default=False), principals: Union[Sequence[str], Sequence[int]] = REQ( json_validator=check_principals, default=EMPTY_PRINCIPALS, ), authorization_errors_fatal: bool = REQ(json_validator=check_bool, default=True), ) -> HttpResponse: realm = user_profile.realm stream_dicts = [] color_map = {} for stream_dict in streams_raw: # 'color' field is optional # check for its presence in the streams_raw first if "color" in stream_dict: color_map[stream_dict["name"]] = stream_dict["color"] stream_dict_copy: StreamDict = {} stream_dict_copy["name"] = stream_dict["name"].strip() # We don't allow newline characters in stream descriptions. if "description" in stream_dict: stream_dict_copy["description"] = stream_dict[ "description"].replace("\n", " ") stream_dict_copy["invite_only"] = invite_only stream_dict_copy["is_web_public"] = is_web_public stream_dict_copy["stream_post_policy"] = stream_post_policy stream_dict_copy[ "history_public_to_subscribers"] = history_public_to_subscribers stream_dict_copy[ "message_retention_days"] = parse_message_retention_days( message_retention_days, Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP) stream_dicts.append(stream_dict_copy) # Validation of the streams arguments, including enforcement of # can_create_streams policy and check_stream_name policy is inside # list_to_streams. existing_streams, created_streams = list_to_streams(stream_dicts, user_profile, autocreate=True) authorized_streams, unauthorized_streams = filter_stream_authorization( user_profile, existing_streams) if len(unauthorized_streams) > 0 and authorization_errors_fatal: raise JsonableError( _("Unable to access stream ({stream_name}).").format( stream_name=unauthorized_streams[0].name, )) # Newly created streams are also authorized for the creator streams = authorized_streams + created_streams if len(principals) > 0: if realm.is_zephyr_mirror_realm and not all(stream.invite_only for stream in streams): raise JsonableError( _("You can only invite other Zephyr mirroring users to private streams." )) if not user_profile.can_subscribe_other_users(): # Guest users case will not be handled here as it will # be handled by the decorator above. raise JsonableError(_("Insufficient permission")) subscribers = { principal_to_user_profile(user_profile, principal) for principal in principals } else: subscribers = {user_profile} (subscribed, already_subscribed) = bulk_add_subscriptions(realm, streams, subscribers, acting_user=user_profile, color_map=color_map) # We can assume unique emails here for now, but we should eventually # convert this function to be more id-centric. email_to_user_profile: Dict[str, UserProfile] = {} result: Dict[str, Any] = dict(subscribed=defaultdict(list), already_subscribed=defaultdict(list)) for sub_info in subscribed: subscriber = sub_info.user stream = sub_info.stream result["subscribed"][subscriber.email].append(stream.name) email_to_user_profile[subscriber.email] = subscriber for sub_info in already_subscribed: subscriber = sub_info.user stream = sub_info.stream result["already_subscribed"][subscriber.email].append(stream.name) result["subscribed"] = dict(result["subscribed"]) result["already_subscribed"] = dict(result["already_subscribed"]) send_messages_for_new_subscribers( user_profile=user_profile, subscribers=subscribers, new_subscriptions=result["subscribed"], email_to_user_profile=email_to_user_profile, created_streams=created_streams, announce=announce, ) result["subscribed"] = dict(result["subscribed"]) result["already_subscribed"] = dict(result["already_subscribed"]) if not authorization_errors_fatal: result["unauthorized"] = [s.name for s in unauthorized_streams] return json_success(result)
def wrapper( request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object ) -> HttpResponse: if not user_profile.has_billing_access: raise JsonableError(_("Must be a billing administrator or an organization owner")) return func(request, user_profile, *args, **kwargs)
def update_subscription_properties_backend( request: HttpRequest, user_profile: UserProfile, subscription_data: List[Dict[str, Any]] = REQ(json_validator=check_list( check_dict([ ("stream_id", check_int), ("property", check_string), ("value", check_union([check_string, check_bool])), ]), ), ), ) -> HttpResponse: """ This is the entry point to changing subscription properties. This is a bulk endpoint: requestors always provide a subscription_data list containing dictionaries for each stream of interest. Requests are of the form: [{"stream_id": "1", "property": "is_muted", "value": False}, {"stream_id": "1", "property": "color", "value": "#c2c2c2"}] """ property_converters = { "color": check_color, "in_home_view": check_bool, "is_muted": check_bool, "desktop_notifications": check_bool, "audible_notifications": check_bool, "push_notifications": check_bool, "email_notifications": check_bool, "pin_to_top": check_bool, "wildcard_mentions_notify": check_bool, } for change in subscription_data: stream_id = change["stream_id"] property = change["property"] value = change["value"] if property not in property_converters: raise JsonableError( _("Unknown subscription property: {}").format(property)) (stream, sub) = access_stream_by_id(user_profile, stream_id) if sub is None: raise JsonableError( _("Not subscribed to stream id {}").format(stream_id)) try: value = property_converters[property](property, value) except ValidationError as error: raise JsonableError(error.message) do_change_subscription_property(user_profile, sub, stream, property, value, acting_user=user_profile) # TODO: Do this more generally, see update_realm_user_settings_defaults.realm.py from zerver.lib.request import RequestNotes request_notes = RequestNotes.get_notes(request) for req_var in request.POST: if req_var not in request_notes.processed_parameters: request_notes.ignored_parameters.add(req_var) result: Dict[str, Any] = {} if len(request_notes.ignored_parameters) > 0: result["ignored_parameters_unsupported"] = list( request_notes.ignored_parameters) return json_success(result)
def _wrapped_view_func( request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object ) -> HttpResponse: if not user_profile.can_edit_user_groups(): raise JsonableError(_("Insufficient permission")) return view_func(request, user_profile, *args, **kwargs)
def update_message_backend( request: HttpRequest, user_profile: UserProfile, message_id: int = REQ(converter=to_non_negative_int, path_only=True), stream_id: Optional[int] = REQ(converter=to_non_negative_int, default=None), topic_name: Optional[str] = REQ_topic(), propagate_mode: Optional[str] = REQ( default="change_one", str_validator=check_string_in(PROPAGATE_MODE_VALUES)), send_notification_to_old_thread: bool = REQ(default=True, json_validator=check_bool), send_notification_to_new_thread: bool = REQ(default=True, json_validator=check_bool), content: Optional[str] = REQ(default=None), ) -> HttpResponse: if not user_profile.realm.allow_message_editing: return json_error( _("Your organization has turned off message editing")) if propagate_mode != "change_one" and topic_name is None and stream_id is None: return json_error(_("Invalid propagate_mode without topic edit")) message, ignored_user_message = access_message(user_profile, message_id) is_no_topic_msg = message.topic_name() == "(no topic)" # You only have permission to edit a message if: # you change this value also change those two parameters in message_edit.js. # 1. You sent it, OR: # 2. This is a topic-only edit for a (no topic) message, OR: # 3. This is a topic-only edit and you are an admin, OR: # 4. This is a topic-only edit and your realm allows users to edit topics. if message.sender == user_profile: pass elif (content is None) and (is_no_topic_msg or user_profile.is_realm_admin or user_profile.realm.allow_community_topic_editing): pass else: raise JsonableError( _("You don't have permission to edit this message")) # If there is a change to the content, check that it hasn't been too long # Allow an extra 20 seconds since we potentially allow editing 15 seconds # past the limit, and in case there are network issues, etc. The 15 comes # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if # you change this value also change those two parameters in message_edit.js. edit_limit_buffer = 20 if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0: deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer if (timezone_now() - message.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has passed")) # If there is a change to the topic, check that the user is allowed to # edit it and that it has not been too long. If this is not the user who # sent the message, they are not the admin, and the time limit for editing # topics is passed, raise an error. if (content is None and message.sender != user_profile and not user_profile.is_realm_admin and not is_no_topic_msg): deadline_seconds = Realm.DEFAULT_COMMUNITY_TOPIC_EDITING_LIMIT_SECONDS + edit_limit_buffer if (timezone_now() - message.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has passed")) if topic_name is None and content is None and stream_id is None: return json_error(_("Nothing to change")) if topic_name is not None: topic_name = topic_name.strip() if topic_name == "": raise JsonableError(_("Topic can't be empty")) rendered_content = None links_for_embed: Set[str] = set() prior_mention_user_ids: Set[int] = set() mention_user_ids: Set[int] = set() mention_data: Optional[MentionData] = None if content is not None: if content.rstrip() == "": content = "(deleted)" content = normalize_body(content) mention_data = MentionData( realm_id=user_profile.realm.id, content=content, ) user_info = get_user_info_for_message_updates(message.id) prior_mention_user_ids = user_info["mention_user_ids"] # We render the message using the current user's realm; since # the cross-realm bots never edit messages, this should be # always correct. # Note: If rendering fails, the called code will raise a JsonableError. rendered_content = render_incoming_message( message, content, user_info["message_user_ids"], user_profile.realm, mention_data=mention_data, ) links_for_embed |= message.links_for_preview mention_user_ids = message.mentions_user_ids new_stream = None number_changed = 0 if stream_id is not None: if not message.is_stream_message(): raise JsonableError(_("Message must be a stream message")) if not user_profile.is_realm_admin: raise JsonableError( _("You don't have permission to move this message")) try: access_stream_by_id(user_profile, message.recipient.type_id) except JsonableError: raise JsonableError( _("You don't have permission to move this message due to missing access to its stream" )) if content is not None: raise JsonableError( _("Cannot change message content while changing stream")) new_stream = access_stream_by_id(user_profile, stream_id, require_active=True)[0] number_changed = do_update_message( user_profile, message, new_stream, topic_name, propagate_mode, send_notification_to_old_thread, send_notification_to_new_thread, content, rendered_content, prior_mention_user_ids, mention_user_ids, mention_data, ) # Include the number of messages changed in the logs request._log_data["extra"] = f"[{number_changed}]" if links_for_embed: event_data = { "message_id": message.id, "message_content": message.content, # The choice of `user_profile.realm_id` rather than # `sender.realm_id` must match the decision made in the # `render_incoming_message` call earlier in this function. "message_realm_id": user_profile.realm_id, "urls": list(links_for_embed), } queue_json_publish("embed_links", event_data) return json_success()
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse: for param in post_params: func_var_name = param.func_var_name if param.path_only: # For path_only parameters, they should already have # been passed via the URL, so there's no need for REQ # to do anything. # # TODO: Either run validators for path_only parameters # or don't declare them using REQ. assert func_var_name in kwargs if func_var_name in kwargs: continue assert func_var_name is not None if param.argument_type == "body": try: val = orjson.loads(request.body) except orjson.JSONDecodeError: raise InvalidJSONError(_("Malformed JSON")) kwargs[func_var_name] = val continue else: # This is a view bug, not a user error, and thus should throw a 500. assert param.argument_type is None, "Invalid argument type" post_var_names = [param.post_var_name] post_var_names += param.aliases default_assigned = False post_var_name: Optional[str] = None for req_var in post_var_names: if req_var in request.POST: val = request.POST[req_var] elif req_var in request.GET: val = request.GET[req_var] else: # This is covered by test_REQ_aliases, but coverage.py # fails to recognize this for some reason. continue # nocoverage if post_var_name is not None: assert req_var is not None raise RequestConfusingParmsError(post_var_name, req_var) post_var_name = req_var if post_var_name is None: post_var_name = param.post_var_name assert post_var_name is not None if param.default is _REQ.NotSpecified: raise RequestVariableMissingError(post_var_name) val = param.default default_assigned = True if param.converter is not None and not default_assigned: try: val = param.converter(val) except JsonableError: raise except Exception: raise RequestVariableConversionError(post_var_name, val) # Validators are like converters, but they don't handle JSON parsing; we do. if param.validator is not None and not default_assigned: try: val = orjson.loads(val) except orjson.JSONDecodeError: raise JsonableError( _('Argument "{}" is not valid JSON.').format( post_var_name)) try: val = param.validator(post_var_name, val) except ValidationError as error: raise JsonableError(error.message) # str_validators is like validator, but for direct strings (no JSON parsing). if param.str_validator is not None and not default_assigned: try: val = param.str_validator(post_var_name, val) except ValidationError as error: raise JsonableError(error.message) kwargs[func_var_name] = val return view_func(request, *args, **kwargs)
def send_to_push_bouncer( method: str, endpoint: str, post_data: Union[Text, Dict[str, Any]], extra_headers: Optional[Dict[str, Any]] = None) -> None: """While it does actually send the notice, this function has a lot of code and comments around error handling for the push notifications bouncer. There are several classes of failures, each with its own potential solution: * Network errors with requests.request. We let those happen normally. * 500 errors from the push bouncer or other unexpected responses; we don't try to parse the response, but do make clear the cause. * 400 errors from the push bouncer. Here there are 2 categories: Our server failed to connect to the push bouncer (should throw) vs. client-side errors like and invalid token. """ url = urllib.parse.urljoin(settings.PUSH_NOTIFICATION_BOUNCER_URL, '/api/v1/remotes/push/' + endpoint) api_auth = requests.auth.HTTPBasicAuth(settings.ZULIP_ORG_ID, settings.ZULIP_ORG_KEY) headers = {"User-agent": "ZulipServer/%s" % (ZULIP_VERSION, )} if extra_headers is not None: headers.update(extra_headers) res = requests.request(method, url, data=post_data, auth=api_auth, timeout=30, verify=True, headers=headers) if res.status_code >= 500: # 500s should be resolved by the people who run the push # notification bouncer service, since they'll get an email # too. For now we email the server admin, but we'll likely # want to do some sort of retry logic eventually. raise PushNotificationBouncerException( _("Received 500 from push notification bouncer")) elif res.status_code >= 400: # If JSON parsing errors, just let that exception happen result_dict = ujson.loads(res.content) msg = result_dict['msg'] if 'code' in result_dict and result_dict[ 'code'] == 'INVALID_ZULIP_SERVER': # Invalid Zulip server credentials should email this server's admins raise PushNotificationBouncerException( _("Push notifications bouncer error: %s") % (msg, )) else: # But most other errors coming from the push bouncer # server are client errors (e.g. never-registered token) # and should be handled as such. raise JsonableError(msg) elif res.status_code != 200: # Anything else is unexpected and likely suggests a bug in # this version of Zulip, so we throw an exception that will # email the server admins. raise PushNotificationBouncerException( "Push notification bouncer returned unexpected status code %s" % (res.status_code, ))
def send_message_backend( request: HttpRequest, user_profile: UserProfile, message_type_name: str = REQ("type"), req_to: Optional[str] = REQ("to", default=None), forged_str: Optional[str] = REQ("forged", default=None, documentation_pending=True), topic_name: Optional[str] = REQ_topic(), message_content: str = REQ("content"), widget_content: Optional[str] = REQ(default=None, documentation_pending=True), realm_str: Optional[str] = REQ("realm_str", default=None, documentation_pending=True), local_id: Optional[str] = REQ(default=None), queue_id: Optional[str] = REQ(default=None), delivery_type: str = REQ("delivery_type", default="send_now", documentation_pending=True), defer_until: Optional[str] = REQ("deliver_at", default=None, documentation_pending=True), tz_guess: Optional[str] = REQ("tz_guess", default=None, documentation_pending=True), time: Optional[float] = REQ(default=None, converter=to_float, documentation_pending=True), ) -> HttpResponse: # If req_to is None, then we default to an # empty list of recipients. message_to: Union[Sequence[int], Sequence[str]] = [] if req_to is not None: if message_type_name == "stream": stream_indicator = extract_stream_indicator(req_to) # For legacy reasons check_send_message expects # a list of streams, instead of a single stream. # # Also, mypy can't detect that a single-item # list populated from a Union[int, str] is actually # a Union[Sequence[int], Sequence[str]]. if isinstance(stream_indicator, int): message_to = [stream_indicator] else: message_to = [stream_indicator] else: message_to = extract_private_recipients(req_to) # Temporary hack: We're transitioning `forged` from accepting # `yes` to accepting `true` like all of our normal booleans. forged = forged_str is not None and forged_str in ["yes", "true"] client = RequestNotes.get_notes(request).client assert client is not None can_forge_sender = user_profile.can_forge_sender if forged and not can_forge_sender: raise JsonableError(_("User not authorized for this query")) realm = None if realm_str and realm_str != user_profile.realm.string_id: # The realm_str parameter does nothing, because it has to match # the user's realm - but we keep it around for backward compatibility. raise JsonableError(_("User not authorized for this query")) if client.name in [ "zephyr_mirror", "irc_mirror", "jabber_mirror", "JabberMirror" ]: # Here's how security works for mirroring: # # For private messages, the message must be (1) both sent and # received exclusively by users in your realm, and (2) # received by the forwarding user. # # For stream messages, the message must be (1) being forwarded # by an API superuser for your realm and (2) being sent to a # mirrored stream. # # The most important security checks are in # `create_mirrored_message_users` below, which checks the # same-realm constraint. if "sender" not in request.POST: raise JsonableError(_("Missing sender")) if message_type_name != "private" and not can_forge_sender: raise JsonableError(_("User not authorized for this query")) # For now, mirroring only works with recipient emails, not for # recipient user IDs. if not all(isinstance(to_item, str) for to_item in message_to): raise JsonableError( _("Mirroring not allowed with recipient user IDs")) # We need this manual cast so that mypy doesn't complain about # create_mirrored_message_users not being able to accept a Sequence[int] # type parameter. message_to = cast(Sequence[str], message_to) try: mirror_sender = create_mirrored_message_users( request, user_profile, message_to) except InvalidMirrorInput: raise JsonableError(_("Invalid mirrored message")) if client.name == "zephyr_mirror" and not user_profile.realm.is_zephyr_mirror_realm: raise JsonableError( _("Zephyr mirroring is not allowed in this organization")) sender = mirror_sender else: if "sender" in request.POST: raise JsonableError(_("Invalid mirrored message")) sender = user_profile if (delivery_type == "send_later" or delivery_type == "remind") and defer_until is None: raise JsonableError( _("Missing deliver_at in a request for delayed message delivery")) if (delivery_type == "send_later" or delivery_type == "remind") and defer_until is not None: return handle_deferred_message( sender, client, message_type_name, message_to, topic_name, message_content, delivery_type, defer_until, tz_guess, forwarder_user_profile=user_profile, realm=realm, ) ret = check_send_message( sender, client, message_type_name, message_to, topic_name, message_content, forged=forged, forged_timestamp=time, forwarder_user_profile=user_profile, realm=realm, local_id=local_id, sender_queue_id=queue_id, widget_content=widget_content, ) return json_success({"id": ret})
def check_add_reaction( user_profile: UserProfile, message_id: int, emoji_name: str, emoji_code: Optional[str], reaction_type: Optional[str], ) -> None: message, user_message = access_message(user_profile, message_id, lock_message=True) if emoji_code is None: # The emoji_code argument is only required for rare corner # cases discussed in the long block comment below. For simple # API clients, we allow specifying just the name, and just # look up the code using the current name->code mapping. emoji_code = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[0] if reaction_type is None: reaction_type = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[1] if Reaction.objects.filter( user_profile=user_profile, message=message, emoji_code=emoji_code, reaction_type=reaction_type, ).exists(): raise JsonableError(_("Reaction already exists.")) query = Reaction.objects.filter( message=message, emoji_code=emoji_code, reaction_type=reaction_type ) if query.exists(): # If another user has already reacted to this message with # same emoji code, we treat the new reaction as a vote for the # existing reaction. So the emoji name used by that earlier # reaction takes precedence over whatever was passed in this # request. This is necessary to avoid a message having 2 # "different" emoji reactions with the same emoji code (and # thus same image) on the same message, which looks ugly. # # In this "voting for an existing reaction" case, we shouldn't # check whether the emoji code and emoji name match, since # it's possible that the (emoji_type, emoji_name, emoji_code) # triple for this existing reaction may not pass validation # now (e.g. because it is for a realm emoji that has been # since deactivated). We still want to allow users to add a # vote any old reaction they see in the UI even if that is a # deactivated custom emoji, so we just use the emoji name from # the existing reaction with no further validation. reaction = query.first() assert reaction is not None emoji_name = reaction.emoji_name else: # Otherwise, use the name provided in this request, but verify # it is valid in the user's realm (e.g. not a deactivated # realm emoji). check_emoji_request(user_profile.realm, emoji_name, emoji_code, reaction_type) if user_message is None: # See called function for more context. create_historical_user_messages(user_id=user_profile.id, message_ids=[message.id]) do_add_reaction(user_profile, message, emoji_name, emoji_code, reaction_type)
def events_register_backend( request: HttpRequest, maybe_user_profile: Union[UserProfile, AnonymousUser], apply_markdown: bool = REQ(default=False, json_validator=check_bool), client_gravatar: bool = REQ(default=True, json_validator=check_bool), slim_presence: bool = REQ(default=False, json_validator=check_bool), all_public_streams: Optional[bool] = REQ(default=None, json_validator=check_bool), include_subscribers: bool = REQ(default=False, json_validator=check_bool), client_capabilities: Optional[Dict[str, bool]] = REQ( json_validator=check_dict( [ # This field was accidentally made required when it was added in v2.0.0-781; # this was not realized until after the release of Zulip 2.1.2. (It remains # required to help ensure backwards compatibility of client code.) ("notification_settings_null", check_bool), ], [ # Any new fields of `client_capabilities` should be optional. Add them here. ("bulk_message_deletion", check_bool), ("user_avatar_url_field_optional", check_bool), ("stream_typing_notifications", check_bool), ("user_settings_object", check_bool), ], value_validator=check_bool, ), default=None, ), event_types: Optional[Sequence[str]] = REQ( json_validator=check_list(check_string), default=None ), fetch_event_types: Optional[Sequence[str]] = REQ( json_validator=check_list(check_string), default=None ), narrow: NarrowT = REQ( json_validator=check_list(check_list(check_string, length=2)), default=[] ), queue_lifespan_secs: int = REQ(json_validator=check_int, default=0, documentation_pending=True), ) -> HttpResponse: if maybe_user_profile.is_authenticated: user_profile = maybe_user_profile assert isinstance(user_profile, UserProfile) realm = user_profile.realm if all_public_streams and not user_profile.can_access_public_streams(): raise JsonableError(_("User not authorized for this query")) all_public_streams = _default_all_public_streams(user_profile, all_public_streams) narrow = _default_narrow(user_profile, narrow) else: user_profile = None realm = get_valid_realm_from_request(request) if not realm.allow_web_public_streams_access(): raise MissingAuthenticationError() all_public_streams = False if client_capabilities is None: client_capabilities = {} client = RequestNotes.get_notes(request).client assert client is not None ret = do_events_register( user_profile, realm, client, apply_markdown, client_gravatar, slim_presence, event_types, queue_lifespan_secs, all_public_streams, narrow=narrow, include_subscribers=include_subscribers, client_capabilities=client_capabilities, fetch_event_types=fetch_event_types, ) return json_success(request, data=ret)
def access_user_group_by_id(user_group_id: int, realm: Realm) -> UserGroup: try: user_group = UserGroup.objects.get(id=user_group_id, realm=realm) except UserGroup.DoesNotExist: raise JsonableError(_("Invalid user group")) return user_group
def wrapper(request: HttpRequest, user_profile: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse: if not user_profile.is_realm_admin: raise JsonableError(_("Must be an organization administrator")) return func(request, user_profile, *args, **kwargs)
def validate_entity(entity: Union[UserProfile, RemoteZulipServer]) -> None: if not isinstance(entity, RemoteZulipServer): raise JsonableError(_("Must validate with valid Zulip server API key"))
def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse: if user_profile.is_guest: raise JsonableError(_("Not allowed for guest users")) return view_func(request, user_profile, *args, **kwargs)
def wrapper(request, user_profile, *args, **kwargs): # type: (HttpRequest, UserProfile, *Any, **Any) -> HttpResponse if not user_profile.is_realm_admin: raise JsonableError(_("Must be a realm administrator")) return func(request, user_profile, *args, **kwargs)