Program: CWE-79
Project: CVE-2022-23656
Buggy source:
import {
    differenceInCalendarDays,
    differenceInHours,
    differenceInMinutes,
    format,
    formatISO,
    isEqual,
    isValid,
    parseISO,
    startOfToday,
} from "date-fns";
import $ from "jquery";
import _ from "lodash";

import render_markdown_time_tooltip from "../templates/markdown_time_tooltip.hbs";

import {$t} from "./i18n";
import {user_settings} from "./user_settings";

let next_timerender_id = 0;

export function clear_for_testing(): void {
    next_timerender_id = 0;
}

// Exported for tests only.
export function get_tz_with_UTC_offset(time: number | Date): string {
    const tz_offset = format(time, "xxx");
    let timezone = new Intl.DateTimeFormat(undefined, {timeZoneName: "short"})
        .formatToParts(time)
        .find(({type}) => type === "timeZoneName")?.value;

    if (timezone === "UTC") {
        return "UTC";
    }

    // When user's locale doesn't match their time zone (eg. en_US for IST),
    // we get `timezone` in the format of'GMT+x:y. We don't want to
    // show that along with (UTC+x:y)
    timezone = /GMT[+-][\d:]*/.test(timezone ?? "") ? "" : timezone;

    const tz_UTC_offset = `(UTC${tz_offset})`;

    if (timezone) {
        return timezone + " " + tz_UTC_offset;
    }
    return tz_UTC_offset;
}

// Given a Date object 'time', returns an object:
// {
//      time_str:        a string for the current human-formatted version
//      formal_time_str: a string for the current formally formatted version
//                          e.g. "Monday, April 15, 2017"
//      needs_update:    a boolean for if it will need to be updated when the
//                       day changes
// }
export type TimeRender = {
    time_str: string;
    formal_time_str: string;
    needs_update: boolean;
};

export function render_now(time: Date, today = new Date()): TimeRender {
    let time_str = "";
    let needs_update = false;
    // render formal time to be used for tippy tooltip
    // "\xa0" is U+00A0 NO-BREAK SPACE.
    // Can't use &nbsp; as that represents the literal string "&nbsp;".
    const formal_time_str = format(time, "EEEE,\u00A0MMMM\u00A0d,\u00A0yyyy");

    // How many days old is 'time'? 0 = today, 1 = yesterday, 7 = a
    // week ago, -1 = tomorrow, etc.

    // Presumably the result of diffDays will be an integer in this
    // case, but round it to be sure before comparing to integer
    // constants.
    const days_old = differenceInCalendarDays(today, time);

    if (days_old === 0) {
        time_str = $t({defaultMessage: "Today"});
        needs_update = true;
    } else if (days_old === 1) {
        time_str = $t({defaultMessage: "Yesterday"});
        needs_update = true;
    } else if (time.getFullYear() !== today.getFullYear()) {
        // For long running servers, searching backlog can get ambiguous
        // without a year stamp. Only show year if message is from an older year
        time_str = format(time, "MMM\u00A0dd,\u00A0yyyy");
        needs_update = false;
    } else {
        // For now, if we get a message from tomorrow, we don't bother
        // rewriting the timestamp when it gets to be tomorrow.
        time_str = format(time, "MMM\u00A0dd");
        needs_update = false;
    }
    return {
        time_str,
        formal_time_str,
        needs_update,
    };
}

// Current date is passed as an argument for unit testing
export function last_seen_status_from_date(
    last_active_date: Date,
    current_date = new Date(),
): string {
    const minutes = differenceInMinutes(current_date, last_active_date);
    if (minutes <= 2) {
        return $t({defaultMessage: "Just now"});
    }
    if (minutes < 60) {
        return $t({defaultMessage: "{minutes} minutes ago"}, {minutes});
    }

    const days_old = differenceInCalendarDays(current_date, last_active_date);
    const hours = Math.floor(minutes / 60);

    if (hours < 24) {
        if (hours === 1) {
            return $t({defaultMessage: "An hour ago"});
        }
        return $t({defaultMessage: "{hours} hours ago"}, {hours});
    }

    if (days_old === 1) {
        return $t({defaultMessage: "Yesterday"});
    }

    if (days_old < 90) {
        return $t({defaultMessage: "{days_old} days ago"}, {days_old});
    } else if (
        days_old > 90 &&
        days_old < 365 &&
        last_active_date.getFullYear() === current_date.getFullYear()
    ) {
        // Online more than 90 days ago, in the same year
        return $t(
            {defaultMessage: "{last_active_date}"},
            {last_active_date: format(last_active_date, "MMM\u00A0dd")},
        );
    }
    return $t(
        {defaultMessage: "{last_active_date}"},
        {last_active_date: format(last_active_date, "MMM\u00A0dd,\u00A0yyyy")},
    );
}

// List of the dates that need to be updated when the day changes.
// Each timestamp is represented as a list of length 2:
//   [id of the span element, Date representing the time]
type UpdateEntry = {
    needs_update: boolean;
    className: string;
    time: Date;
    time_above?: Date;
};
let update_list: UpdateEntry[] = [];

// The time at the beginning of the day, when the timestamps were updated.
// Represented as a Date with hour, minute, second, millisecond 0.
let last_update: Date;

export function initialize(): void {
    last_update = startOfToday();
}

// time_above is an optional argument, to support dates that look like:
// --- ▲ Yesterday ▲ ------ ▼ Today ▼ ---
function maybe_add_update_list_entry(entry: UpdateEntry): void {
    if (entry.needs_update) {
        update_list.push(entry);
    }
}

function render_date_span(
    elem: JQuery,
    rendered_time: TimeRender,
    rendered_time_above?: TimeRender,
): JQuery {
    elem.text("");
    if (rendered_time_above !== undefined) {
        elem.append(
            '<i class="date-direction fa fa-caret-up"></i>',
            _.escape(rendered_time_above.time_str),
            '<hr class="date-line">',
            '<i class="date-direction fa fa-caret-down"></i>',
            _.escape(rendered_time.time_str),
        );
        return elem;
    }
    elem.append(_.escape(rendered_time.time_str));
    return elem.attr("data-tippy-content", rendered_time.formal_time_str);
}

// Given an Date object 'time', return a DOM node that initially
// displays the human-formatted date, and is updated automatically as
// necessary (e.g. changing "Today" to "Yesterday" to "Jul 1").
// If two dates are given, it renders them as:
// --- ▲ Yesterday ▲ ------ ▼ Today ▼ ---

// (What's actually spliced into the message template is the contents
// of this DOM node as HTML, so effectively a copy of the node. That's
// okay since to update the time later we look up the node by its id.)
export function render_date(time: Date, time_above: Date | undefined, today: Date): JQuery {
    const className = `timerender${next_timerender_id}`;
    next_timerender_id += 1;
    const rendered_time = render_now(time, today);
    let node = $("<span />").attr("class", className);
    if (time_above !== undefined) {
        const rendered_time_above = render_now(time_above, today);
        node = render_date_span(node, rendered_time, rendered_time_above);
    } else {
        node = render_date_span(node, rendered_time);
    }
    maybe_add_update_list_entry({
        needs_update: rendered_time.needs_update,
        className,
        time,
        time_above,
    });
    return node;
}

// Renders the timestamp returned by the <time:> Markdown syntax.
export function render_markdown_timestamp(time: number | Date): {
    text: string;
    tooltip_content: string;
} {
    const hourformat = user_settings.twenty_four_hour_time ? "HH:mm" : "h:mm a";
    const timestring = format(time, "E, MMM d yyyy, " + hourformat);

    const tz_offset_str = get_tz_with_UTC_offset(time);
    const tooltip_html_content = render_markdown_time_tooltip({tz_offset_str});

    return {
        text: timestring,
        tooltip_content: tooltip_html_content,
    };
}

// This isn't expected to be called externally except manually for
// testing purposes.
export function update_timestamps(): void {
    const today = startOfToday();
    if (!isEqual(today, last_update)) {
        const to_process = update_list;
        update_list = [];

        for (const entry of to_process) {
            const className = entry.className;
            const elements = $(`.${CSS.escape(className)}`);
            // The element might not exist any more (because it
            // was in the zfilt table, or because we added
            // messages above it and re-collapsed).
            if (elements.length > 0) {
                const time = entry.time;
                const time_above = entry.time_above;
                const rendered_time = render_now(time, today);
                const rendered_time_above = time_above ? render_now(time_above, today) : undefined;
                for (const element of elements) {
                    render_date_span($(element), rendered_time, rendered_time_above);
                }
                maybe_add_update_list_entry({
                    needs_update: rendered_time.needs_update,
                    className,
                    time,
                    time_above,
                });
            }
        }

        last_update = today;
    }
}

setInterval(update_timestamps, 60 * 1000);

// Transform a Unix timestamp into a ISO 8601 formatted date string.
//   Example: 1978-10-31T13:37:42Z
export function get_full_time(timestamp: number): string {
    return formatISO(timestamp * 1000);
}

export function get_timestamp_for_flatpickr(timestring: string): Date {
    let timestamp;
    try {
        // If there's already a valid time in the compose box,
        // we use it to initialize the flatpickr instance.
        timestamp = parseISO(timestring);
    } finally {
        // Otherwise, default to showing the current time.
        if (!timestamp || !isValid(timestamp)) {
            timestamp = new Date();
        }
    }
    return timestamp;
}

export function stringify_time(time: number | Date): string {
    if (user_settings.twenty_four_hour_time) {
        return format(time, "HH:mm");
    }
    return format(time, "h:mm a");
}

export function format_time_modern(time: number | Date, today = new Date()): String {
    const hours = differenceInHours(today, time);
    const days_old = differenceInCalendarDays(today, time);

    if (time > today) {
        /* For timestamps in the future, we always show the year*/
        return format(time, "MMM\u00A0dd,\u00A0yyyy");
    } else if (hours < 24) {
        return stringify_time(time);
    } else if (days_old === 1) {
        return $t({defaultMessage: "Yesterday"});
    } else if (days_old < 7) {
        return format(time, "EEEE");
    } else if (days_old <= 180) {
        return format(time, "MMM\u00A0dd");
    }

    return format(time, "MMM\u00A0dd,\u00A0yyyy");
}

// this is for rendering absolute time based off the preferences for twenty-four
// hour time in the format of "%mmm %d, %h:%m %p".
export function absolute_time(timestamp: number, today = new Date()): string {
    const date = new Date(timestamp);
    const is_older_year = today.getFullYear() - date.getFullYear() > 0;
    const H_24 = user_settings.twenty_four_hour_time;

    return format(
        date,
        is_older_year
            ? H_24
                ? "MMM d, yyyy HH:mm"
                : "MMM d, yyyy hh:mm a"
            : H_24
            ? "MMM d HH:mm"
            : "MMM d hh:mm a",
    );
}

export function get_full_datetime(time: Date): string {
    const time_options: Intl.DateTimeFormatOptions = {timeStyle: "medium"};

    if (user_settings.twenty_four_hour_time) {
        time_options.hourCycle = "h24";
    }

    const date_string = time.toLocaleDateString();
    let time_string = time.toLocaleTimeString(undefined, time_options);

    const tz_offset_str = get_tz_with_UTC_offset(time);

    time_string = time_string + " " + tz_offset_str;

    return $t({defaultMessage: "{date} at {time}"}, {date: date_string, time: time_string});
}


Fixed version (ground truth):
import {
    differenceInCalendarDays,
    differenceInHours,
    differenceInMinutes,
    format,
    formatISO,
    isEqual,
    isValid,
    parseISO,
    startOfToday,
} from "date-fns";
import $ from "jquery";
import _ from "lodash";

import render_markdown_time_tooltip from "../templates/markdown_time_tooltip.hbs";

import {$t} from "./i18n";
import {user_settings} from "./user_settings";

let next_timerender_id = 0;

export function clear_for_testing(): void {
    next_timerender_id = 0;
}

// Exported for tests only.
export function get_tz_with_UTC_offset(time: number | Date): string {
    const tz_offset = format(time, "xxx");
    let timezone = new Intl.DateTimeFormat(undefined, {timeZoneName: "short"})
        .formatToParts(time)
        .find(({type}) => type === "timeZoneName")?.value;

    if (timezone === "UTC") {
        return "UTC";
    }

    // When user's locale doesn't match their time zone (eg. en_US for IST),
    // we get `timezone` in the format of'GMT+x:y. We don't want to
    // show that along with (UTC+x:y)
    timezone = /GMT[+-][\d:]*/.test(timezone ?? "") ? "" : timezone;

    const tz_UTC_offset = `(UTC${tz_offset})`;

    if (timezone) {
        return timezone + " " + tz_UTC_offset;
    }
    return tz_UTC_offset;
}

// Given a Date object 'time', returns an object:
// {
//      time_str:        a string for the current human-formatted version
//      formal_time_str: a string for the current formally formatted version
//                          e.g. "Monday, April 15, 2017"
//      needs_update:    a boolean for if it will need to be updated when the
//                       day changes
// }
export type TimeRender = {
    time_str: string;
    formal_time_str: string;
    needs_update: boolean;
};

export function render_now(time: Date, today = new Date()): TimeRender {
    let time_str = "";
    let needs_update = false;
    // render formal time to be used for tippy tooltip
    // "\xa0" is U+00A0 NO-BREAK SPACE.
    // Can't use &nbsp; as that represents the literal string "&nbsp;".
    const formal_time_str = format(time, "EEEE,\u00A0MMMM\u00A0d,\u00A0yyyy");

    // How many days old is 'time'? 0 = today, 1 = yesterday, 7 = a
    // week ago, -1 = tomorrow, etc.

    // Presumably the result of diffDays will be an integer in this
    // case, but round it to be sure before comparing to integer
    // constants.
    const days_old = differenceInCalendarDays(today, time);

    if (days_old === 0) {
        time_str = $t({defaultMessage: "Today"});
        needs_update = true;
    } else if (days_old === 1) {
        time_str = $t({defaultMessage: "Yesterday"});
        needs_update = true;
    } else if (time.getFullYear() !== today.getFullYear()) {
        // For long running servers, searching backlog can get ambiguous
        // without a year stamp. Only show year if message is from an older year
        time_str = format(time, "MMM\u00A0dd,\u00A0yyyy");
        needs_update = false;
    } else {
        // For now, if we get a message from tomorrow, we don't bother
        // rewriting the timestamp when it gets to be tomorrow.
        time_str = format(time, "MMM\u00A0dd");
        needs_update = false;
    }
    return {
        time_str,
        formal_time_str,
        needs_update,
    };
}

// Current date is passed as an argument for unit testing
export function last_seen_status_from_date(
    last_active_date: Date,
    current_date = new Date(),
): string {
    const minutes = differenceInMinutes(current_date, last_active_date);
    if (minutes <= 2) {
        return $t({defaultMessage: "Just now"});
    }
    if (minutes < 60) {
        return $t({defaultMessage: "{minutes} minutes ago"}, {minutes});
    }

    const days_old = differenceInCalendarDays(current_date, last_active_date);
    const hours = Math.floor(minutes / 60);

    if (hours < 24) {
        if (hours === 1) {
            return $t({defaultMessage: "An hour ago"});
        }
        return $t({defaultMessage: "{hours} hours ago"}, {hours});
    }

    if (days_old === 1) {
        return $t({defaultMessage: "Yesterday"});
    }

    if (days_old < 90) {
        return $t({defaultMessage: "{days_old} days ago"}, {days_old});
    } else if (
        days_old > 90 &&
        days_old < 365 &&
        last_active_date.getFullYear() === current_date.getFullYear()
    ) {
        // Online more than 90 days ago, in the same year
        return $t(
            {defaultMessage: "{last_active_date}"},
            {last_active_date: format(last_active_date, "MMM\u00A0dd")},
        );
    }
    return $t(
        {defaultMessage: "{last_active_date}"},
        {last_active_date: format(last_active_date, "MMM\u00A0dd,\u00A0yyyy")},
    );
}

// List of the dates that need to be updated when the day changes.
// Each timestamp is represented as a list of length 2:
//   [id of the span element, Date representing the time]
type UpdateEntry = {
    needs_update: boolean;
    className: string;
    time: Date;
    time_above?: Date;
};
let update_list: UpdateEntry[] = [];

// The time at the beginning of the day, when the timestamps were updated.
// Represented as a Date with hour, minute, second, millisecond 0.
let last_update: Date;

export function initialize(): void {
    last_update = startOfToday();
}

// time_above is an optional argument, to support dates that look like:
// --- ▲ Yesterday ▲ ------ ▼ Today ▼ ---
function maybe_add_update_list_entry(entry: UpdateEntry): void {
    if (entry.needs_update) {
        update_list.push(entry);
    }
}

function render_date_span(
    elem: JQuery,
    rendered_time: TimeRender,
    rendered_time_above?: TimeRender,
): JQuery {
    elem.text("");
    if (rendered_time_above !== undefined) {
        elem.append(
            '<i class="date-direction fa fa-caret-up"></i>',
            _.escape(rendered_time_above.time_str),
            '<hr class="date-line">',
            '<i class="date-direction fa fa-caret-down"></i>',
            _.escape(rendered_time.time_str),
        );
        return elem;
    }
    elem.append(_.escape(rendered_time.time_str));
    return elem.attr("data-tippy-content", rendered_time.formal_time_str);
}

// Given an Date object 'time', return a DOM node that initially
// displays the human-formatted date, and is updated automatically as
// necessary (e.g. changing "Today" to "Yesterday" to "Jul 1").
// If two dates are given, it renders them as:
// --- ▲ Yesterday ▲ ------ ▼ Today ▼ ---

// (What's actually spliced into the message template is the contents
// of this DOM node as HTML, so effectively a copy of the node. That's
// okay since to update the time later we look up the node by its id.)
export function render_date(time: Date, time_above: Date | undefined, today: Date): JQuery {
    const className = `timerender${next_timerender_id}`;
    next_timerender_id += 1;
    const rendered_time = render_now(time, today);
    let node = $("<span />").attr("class", className);
    if (time_above !== undefined) {
        const rendered_time_above = render_now(time_above, today);
        node = render_date_span(node, rendered_time, rendered_time_above);
    } else {
        node = render_date_span(node, rendered_time);
    }
    maybe_add_update_list_entry({
        needs_update: rendered_time.needs_update,
        className,
        time,
        time_above,
    });
    return node;
}

// Renders the timestamp returned by the <time:> Markdown syntax.
export function render_markdown_timestamp(time: number | Date): {
    text: string;
    tooltip_content_html: string;
} {
    const hourformat = user_settings.twenty_four_hour_time ? "HH:mm" : "h:mm a";
    const timestring = format(time, "E, MMM d yyyy, " + hourformat);

    const tz_offset_str = get_tz_with_UTC_offset(time);
    const tooltip_content_html = render_markdown_time_tooltip({tz_offset_str});

    return {
        text: timestring,
        tooltip_content_html: tooltip_content_html,
    };
}

// This isn't expected to be called externally except manually for
// testing purposes.
export function update_timestamps(): void {
    const today = startOfToday();
    if (!isEqual(today, last_update)) {
        const to_process = update_list;
        update_list = [];

        for (const entry of to_process) {
            const className = entry.className;
            const elements = $(`.${CSS.escape(className)}`);
            // The element might not exist any more (because it
            // was in the zfilt table, or because we added
            // messages above it and re-collapsed).
            if (elements.length > 0) {
                const time = entry.time;
                const time_above = entry.time_above;
                const rendered_time = render_now(time, today);
                const rendered_time_above = time_above ? render_now(time_above, today) : undefined;
                for (const element of elements) {
                    render_date_span($(element), rendered_time, rendered_time_above);
                }
                maybe_add_update_list_entry({
                    needs_update: rendered_time.needs_update,
                    className,
                    time,
                    time_above,
                });
            }
        }

        last_update = today;
    }
}

setInterval(update_timestamps, 60 * 1000);

// Transform a Unix timestamp into a ISO 8601 formatted date string.
//   Example: 1978-10-31T13:37:42Z
export function get_full_time(timestamp: number): string {
    return formatISO(timestamp * 1000);
}

export function get_timestamp_for_flatpickr(timestring: string): Date {
    let timestamp;
    try {
        // If there's already a valid time in the compose box,
        // we use it to initialize the flatpickr instance.
        timestamp = parseISO(timestring);
    } finally {
        // Otherwise, default to showing the current time.
        if (!timestamp || !isValid(timestamp)) {
            timestamp = new Date();
        }
    }
    return timestamp;
}

export function stringify_time(time: number | Date): string {
    if (user_settings.twenty_four_hour_time) {
        return format(time, "HH:mm");
    }
    return format(time, "h:mm a");
}

export function format_time_modern(time: number | Date, today = new Date()): String {
    const hours = differenceInHours(today, time);
    const days_old = differenceInCalendarDays(today, time);

    if (time > today) {
        /* For timestamps in the future, we always show the year*/
        return format(time, "MMM\u00A0dd,\u00A0yyyy");
    } else if (hours < 24) {
        return stringify_time(time);
    } else if (days_old === 1) {
        return $t({defaultMessage: "Yesterday"});
    } else if (days_old < 7) {
        return format(time, "EEEE");
    } else if (days_old <= 180) {
        return format(time, "MMM\u00A0dd");
    }

    return format(time, "MMM\u00A0dd,\u00A0yyyy");
}

// this is for rendering absolute time based off the preferences for twenty-four
// hour time in the format of "%mmm %d, %h:%m %p".
export function absolute_time(timestamp: number, today = new Date()): string {
    const date = new Date(timestamp);
    const is_older_year = today.getFullYear() - date.getFullYear() > 0;
    const H_24 = user_settings.twenty_four_hour_time;

    return format(
        date,
        is_older_year
            ? H_24
                ? "MMM d, yyyy HH:mm"
                : "MMM d, yyyy hh:mm a"
            : H_24
            ? "MMM d HH:mm"
            : "MMM d hh:mm a",
    );
}

export function get_full_datetime(time: Date): string {
    const time_options: Intl.DateTimeFormatOptions = {timeStyle: "medium"};

    if (user_settings.twenty_four_hour_time) {
        time_options.hourCycle = "h24";
    }

    const date_string = time.toLocaleDateString();
    let time_string = time.toLocaleTimeString(undefined, time_options);

    const tz_offset_str = get_tz_with_UTC_offset(time);

    time_string = time_string + " " + tz_offset_str;

    return $t({defaultMessage: "{date} at {time}"}, {date: date_string, time: time_string});
}