<commit_before>/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2018 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

import '../lib/patternfly/patternfly-4-cockpit.scss';
import cockpit from "cockpit";
import React from "react";
import ReactDOM from "react-dom";
import {
    Alert, Button,
    Breadcrumb, BreadcrumbItem,
    Checkbox,
    Card, CardTitle, CardHeader, CardActions, CardBody,
    DataList, DataListItem, DataListCell, DataListItemRow, DataListCheck, DataListItemCells,
    Flex, FlexItem,
    Form, FormGroup, FormHelperText,
    Radio, Split, SplitItem, Stack,
    TextInput, Title, Toolbar, ToolbarContent, ToolbarItem,
    Tooltip, Page, PageSection, PageSectionVariants, Modal,
} from '@patternfly/react-core';
import { ExclamationCircleIcon, TrashIcon } from '@patternfly/react-icons';

import firewall from "./firewall-client.js";
import { ListingTable } from 'cockpit-components-table.jsx';
import { ModalError } from "cockpit-components-inline-notification.jsx";
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
import { FirewallSwitch } from "./firewall-switch.jsx";

import { superuser } from "superuser";

import "./networking.scss";

const _ = cockpit.gettext;

superuser.reload_page_on_change();

function serviceRow(props) {
    let tcp = props.service.ports.filter(p => p.protocol.toUpperCase() == 'TCP');
    let udp = props.service.ports.filter(p => p.protocol.toUpperCase() == 'UDP');

    for (const s of props.service.includes) {
        if (firewall.services[s]) {
            tcp = tcp.concat(firewall.services[s].ports.filter(p => p.protocol.toUpperCase() == 'TCP'));
            udp = udp.concat(firewall.services[s].ports.filter(p => p.protocol.toUpperCase() == 'UDP'));
        }
    }

    function onRemoveService(event) {
        if (event.button !== 0)
            return;

        props.onRemoveService(props.service.id);
        event.stopPropagation();
    }

    const deleteButton = <Button key={props.service.id + "-delete-button"} variant="danger" onClick={onRemoveService} aria-label={cockpit.format(_("Remove service $0"), props.service.id)}><TrashIcon /></Button>;

    const columns = [
        {
            title: props.service.id, header: true
        },
        {
            title: <div key={props.service.id + "tcp"}>
                { tcp.map(p => p.port).join(', ') }
            </div>
        },
        {
            title: <div key={props.service.id + "udp"}>
                { udp.map(p => p.port).join(', ') }
            </div>
        },
    ];

    let description, includes;
    if (props.service.description)
        description = <p>{props.service.description}</p>;

    if (props.service.includes.length > 0) {
        includes = <>
            <h5>Included Services</h5>
            <ul>{props.service.includes.map(s => {
                const service = firewall.services[s];
                if (service && service.description)
                    return <li key={service.id}><strong>{service.id}</strong>: {service.description}</li>;
            })} </ul></>;
    }
    const simpleBody = <Split>
        <SplitItem key="description" isFilled>{description}{includes}</SplitItem>
        {!props.readonly && <SplitItem key="actions">{deleteButton}</SplitItem>}
    </Split>;

    return ({
        props: { key: props.service.id, 'data-row-id': props.service.id },
        columns,
        hasPadding: true,
        expandedContent: simpleBody,
    });
}

function portRow(props) {
    const columns = [
        {
            title: <i key={props.zone.id + "-additional-ports"}>{ _("Additional ports") }</i>
        },
        {
            title: props.zone.ports
                    .filter(p => p.protocol === "tcp")
                    .map(p => p.port)
                    .join(", ")
        },
        {
            title: props.zone.ports
                    .filter(p => p.protocol === "udp")
                    .map(p => p.port)
                    .join(", ")
        },
    ];
    return ({
        props: { key: props.zone.id + "-ports", 'data-row-id': props.zone.id + "-ports" },
        columns
    });
}

function ZoneSection(props) {
    function onRemoveZone(event) {
        if (event.button !== 0)
            return;

        event.stopPropagation();
        props.onRemoveZone(props.zone.id);
    }

    let deleteButton;
    if (props.readonly) {
        deleteButton = (
            <Tooltip id="tip-auth" content={ _("You are not authorized to modify the firewall.") }>
                <span>
                    <Button variant="danger"
                            aria-label={cockpit.format(_("Not authorized to remove zone $0"), props.zone.id)}
                            isDisabled icon={<TrashIcon />} />
                </span>
            </Tooltip>
        );
    } else {
        deleteButton = <Button variant="danger" onClick={onRemoveZone}
                               aria-label={cockpit.format(_("Remove zone $0"), props.zone.id)}
                               icon={<TrashIcon />} />;
    }

    const addServiceAction = (
        <Button variant="primary" onClick={() => props.openServicesDialog(props.zone.id, props.zone.id)} className="add-services-button" aria-label={cockpit.format(_("Add services to zone $0"), props.zone.id)}>
            {_("Add services")}
        </Button>
    );

    return <Card className="zone-section" data-id={props.zone.id}>
        <CardHeader className="zone-section-heading">
            <CardTitle>
                <h4>{ cockpit.format(_("$0 zone"), props.zone.id) }</h4>
                <div className="zone-section-targets">
                    { props.zone.interfaces.length > 0 && <span className="zone-section-target"><strong>{_("Interfaces")}</strong> {props.zone.interfaces.join(", ")}</span> }
                    { props.zone.source.length > 0 && <span className="zone-section-target"><strong>{_("Addresses")}</strong> {props.zone.source.join(", ")}</span> }
                </div>
            </CardTitle>
            { !firewall.readonly && <CardActions className="zone-section-buttons">{deleteButton}{addServiceAction}</CardActions> }
        </CardHeader>
        {props.zone.services.length > 0 &&
        <CardBody className="contains-list">
            <ListingTable columns={[{ title: _("Service"), props: { width: 40 } }, { title: _("TCP"), props: { width: 30 } }, { title: _("UDP"), props: { width: 30 } }]}
                          id={props.zone.id}
                          aria-label={props.zone.id}
                          variant="compact"
                          emptyCaption={_("There are no active services in this zone")}
                          rows={
                              props.zone.services.map(s => {
                                  if (s in firewall.services)
                                      return serviceRow({
                                          key: firewall.services[s].id,
                                          service: firewall.services[s],
                                          onRemoveService: service => props.onRemoveService(props.zone.id, service),
                                          readonly: firewall.readonly
                                      });
                              }).concat(
                                  props.zone.ports.length > 0
                                      ? portRow({
                                          key: props.zone.id + "-ports",
                                          zone: props.zone,
                                          readonly: firewall.readonly
                                      }) : [])
                                      .filter(Boolean)}

            />
        </CardBody>}
    </Card>;
}

class SearchInput extends React.Component {
    constructor(props) {
        super(props);
        this.onValueChanged = this.onValueChanged.bind(this);
        this.state = { value: props.value || "" };
    }

    onValueChanged(value) {
        this.setState({ value });

        if (this.timer)
            window.clearTimeout(this.timer);

        this.timer = window.setTimeout(() => {
            this.props.onChange(value);
            this.timer = null;
        }, 300);
    }

    render() {
        return (
            <Toolbar>
                <ToolbarContent>
                    <ToolbarItem variant="label">
                        {_("Filter services")}
                    </ToolbarItem>
                    <ToolbarItem>
                        <TextInput type="search"
                                   id={this.props.id}
                                   onChange={this.onValueChanged}
                                   value={this.state.value}
                        />
                    </ToolbarItem>
                </ToolbarContent>
            </Toolbar>
        );
    }
}

const renderPorts = service => {
    const tcpPorts = [];
    const udpPorts = [];
    function addPorts(ports) {
        for (const port of ports) {
            if (port.protocol === "tcp")
                tcpPorts.push(port.port);
            else
                udpPorts.push(port.port);
        }
    }
    addPorts(service.ports);
    for (const s of service.includes)
        addPorts(firewall.services[s].ports);

    return (
        <div className="service-list-item-text">
            { tcpPorts.length > 0 && <span className="service-ports tcp"><strong>TCP: </strong>{ tcpPorts.join(', ') }</span> }
            { udpPorts.length > 0 && <span className="service-ports udp"><strong>UDP: </strong>{ udpPorts.join(', ') }</span> }
        </div>
    );
};

class AddServicesModal extends React.Component {
    constructor() {
        super();

        this.state = {
            services: null,
            selected: new Set(),
            filter: "",
            custom: false,
            generate_custom_id: true,
            tcp_error: "",
            udp_error: "",
            avail_services: null,
            custom_id: "",
            custom_description: "",
            custom_tcp_ports: [],
            custom_udp_ports: [],
            custom_tcp_value: "",
            custom_udp_value: "",
            dialogError: null,
            dialogErrorDetail: null,
        };
        this.save = this.save.bind(this);
        this.onFilterChanged = this.onFilterChanged.bind(this);
        this.onToggleService = this.onToggleService.bind(this);
        this.setId = this.setId.bind(this);
        this.setDescription = this.setDescription.bind(this);
        this.getName = this.getName.bind(this);
        this.validate = this.validate.bind(this);
        this.createPorts = this.createPorts.bind(this);
        this.parseServices = this.parseServices.bind(this);
        this.onToggleType = this.onToggleType.bind(this);
    }

    createPorts() {
        const ret = [];
        this.state.custom_tcp_ports.forEach(port => ret.push([port, 'tcp']));
        this.state.custom_udp_ports.forEach(port => ret.push([port, 'udp']));
        return ret;
    }

    save(event) {
        let p;
        if (this.state.custom) {
            p = firewall.createService(this.state.custom_id, this.createPorts(), this.props.zoneId, this.state.custom_description);
        } else {
            p = firewall.addServices(this.props.zoneId, [...this.state.selected]);
        }
        p.then(() => this.props.close())
                .catch(error => {
                    this.setState({
                        dialogError: this.state.custom ? _("Failed to add port") : _("Failed to add service"),
                        dialogErrorDetail: error.name + ": " + error.message,
                    });
                });

        if (event)
            event.preventDefault();
        return false;
    }

    onToggleService(event, serviceId) {
        const service = serviceId;
        const enabled = event.target.checked;

        this.setState(oldState => {
            const selected = new Set(oldState.selected);

            if (enabled)
                selected.add(service);
            else
                selected.delete(service);

            return {
                selected: selected
            };
        });
    }

    /* Create list of services from /etc/services type file
     *
     * Return dictionary of services:
     *  - key => port number or port alias (80/http)
     *  - item => dictionary with 3 compulsory items:
     *      - name => port alias (http)
     *      - port => port number (80)
     *      - type => list of types (tcp/udp...)
     *      - description => _may be not present_ (Web Server)
     */
    parseServices(content) {
        if (!content) {
            console.warn("Couldn't read /etc/services");
            return [];
        }

        const ret = {};
        content.split('\n').forEach(line => {
            if (!line || line.startsWith("#"))
                return;
            const m = line.match(/^(\S+)\s+(\d+)\/(\S+).*?(#(.*))?$/);
            const new_port = { name: m[1], port: m[2], type: [m[3]] };
            if (m.length > 5 && m[5])
                new_port.description = m[5].trim();
            if (ret[m[1]])
                ret[m[1]].type.push(new_port.type[0]);
            else
                ret[m[1]] = new_port;
            if (ret[m[2]])
                ret[m[2]].type.push(new_port.type[0]);
            else
                ret[m[2]] = new_port;
        });
        return ret;
    }

    setId(value) {
        this.setState({
            custom_id: value,
            generate_custom_id: value.length === 0,
        });
    }

    setDescription(value) {
        this.setState({
            custom_description: value
        });
    }

    getName(port) {
        const known = this.state.avail_services[port];
        if (known)
            return known.name;
        else
            return port;
    }

    getPortNumber(port, type, avail) {
        if (!avail) {
            const num_p = Number(port);
            if (isNaN(num_p))
                return [0, _("Unknown service name")];
            else if (num_p <= 0 || num_p > 65535)
                return [0, _("Invalid port number")];
            else
                return [port, ""];
        } else if (avail.type.indexOf(type) < 0)
            return [0, _("Port number and type do not match")];
        else {
            return [avail.port, ""];
        }
    }

    validate(value, event) {
        let error = "";
        let targets = ['tcp', 'custom_tcp_ports', 'tcp_error', 'custom_tcp_value'];
        if (event.target.id === "udp-ports")
            targets = ['udp', 'custom_udp_ports', 'udp_error', 'custom_udp_value'];
        const new_ports = [];
        const event_id = event.target.id;

        this.setState(oldState => {
            const ports = value.split(',');
            ports.forEach((port) => {
                port = port.trim();
                if (!port)
                    return;
                let ports;
                if (port.indexOf("-") > -1) {
                    ports = port.split("-");
                    if (ports.length != 2) {
                        error = _("Invalid range");
                        return;
                    }
                    [ports[0], error] = this.getPortNumber(ports[0], targets[0], oldState.avail_services[ports[0]]);
                    if (!error) {
                        [ports[1], error] = this.getPortNumber(ports[1], targets[0], oldState.avail_services[ports[1]]);
                        if (!error) {
                            if (Number(ports[0]) >= Number(ports[1]))
                                error = _("Range must be strictly ordered");
                            else
                                new_ports.push(ports[0] + "-" + ports[1]);
                        }
                    }
                } else {
                    [ports, error] = this.getPortNumber(port, targets[0], oldState.avail_services[port]);
                    if (!error)
                        new_ports.push(ports);
                }
            });
            const newState = {
                [targets[1]]: new_ports,
                [targets[2]]: error,
                [targets[3]]: value
            };

            let all_ports = new_ports.concat(oldState.custom_udp_ports);
            if (event_id === "udp-ports")
                all_ports = oldState.custom_tcp_ports.concat(new_ports);

            if (oldState.generate_custom_id) {
                if (all_ports.length > 0)
                    newState.custom_id = "custom--" + all_ports.map(this.getName).join('-');
                else
                    newState.custom_id = "";
            }

            return newState;
        });
    }

    onToggleType(value, event) {
        this.setState({
            custom: event.target.value === "ports"
        });
    }

    componentDidMount() {
        firewall.getAvailableServices()
                .then(services => this.setState({ services }));
        cockpit.file('/etc/services').read()
                .done(content => this.setState({
                    avail_services: this.parseServices(content)
                }));
    }

    onFilterChanged(value) {
        this.setState({ filter: value.toLowerCase() });
    }

    render() {
        let services;
        if (this.state.filter && this.state.services && !isNaN(this.state.filter))
            services = this.state.services.filter(s => {
                for (const port of s.ports)
                    if (port.port === this.state.filter)
                        return true;
                return false;
            });
        else if (this.state.filter && this.state.services)
            services = this.state.services.filter(s => s.id.indexOf(this.state.filter) > -1);
        else
            services = this.state.services;

        // hide services which have been enabled in the zone
        if (services)
            services = services.filter(s => firewall.zones[this.props.zoneId].services.indexOf(s.id) === -1);

        const addText = this.state.custom ? _("Add ports") : _("Add services");
        const titleText = this.state.custom ? cockpit.format(_("Add ports to $0 zone"), this.props.zoneName) : cockpit.format(_("Add services to $0 zone"), this.props.zoneName);
        return (
            <Modal id="add-services-dialog" isOpen
                   position="top" variant="medium"
                   onClose={this.props.close}
                   title={titleText}
                   footer={<>
                       {
                           this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />
                       }
                       { !this.state.custom ||
                           <Alert variant="warning"
                               isInline
                               title={_("Adding custom ports will reload firewalld. A reload will result in the loss of any runtime-only configuration!")} />
                       }
                       <Button variant='primary' onClick={this.save} aria-label={titleText}>
                           {addText}
                       </Button>
                       <Button variant='link' className='btn-cancel' onClick={this.props.close}>
                           {_("Cancel")}
                       </Button>
                   </>}
            >
                <Form isHorizontal onSubmit={this.save}>
                    <FormGroup className="add-services-dialog-type" isInline>
                        <Radio name="type"
                               id="add-services-dialog--services"
                               value="services"
                               isChecked={!this.state.custom}
                               onChange={this.onToggleType}
                               label={_("Services")} />
                        <Radio name="type"
                               id="add-services-dialog--ports"
                               value="ports"
                               isChecked={this.state.custom}
                               onChange={this.onToggleType}
                               isDisabled={this.state.avail_services == null}
                               label={_("Custom ports")} />
                    </FormGroup>
                    { this.state.custom ||
                        <div>
                            { services ? (
                                <>
                                    <SearchInput id="filter-services-input"
                                                 value={this.state.filter}
                                                 onChange={this.onFilterChanged} />
                                    <DataList className="service-list" isCompact>
                                        {services.map(s => (
                                            <DataListItem key={s.id} aria-labelledby={s.id}>
                                                <DataListItemRow>
                                                    <DataListCheck aria-labelledby={s.id}
                                                                   isChecked={this.state.selected.has(s.id)}
                                                                   onChange={(value, event) => this.onToggleService(event, s.id)}
                                                                   id={"firewall-service-" + s.id}
                                                                   name={s.id + "-checkbox"} />
                                                    <DataListItemCells
                                                            dataListCells={[
                                                                <DataListCell key="service-list-item">
                                                                    <label htmlFor={"firewall-service-" + s.id}
                                                                           className="service-list-iteam-heading">
                                                                        {s.id}
                                                                    </label>
                                                                    {renderPorts(s)}
                                                                </DataListCell>,
                                                            ]} />
                                                </DataListItemRow>
                                            </DataListItem>
                                        ))}
                                    </DataList>
                                </>
                            ) : (
                                <EmptyStatePanel loading />
                            )}
                        </div>
                    }
                    { !this.state.custom ||
                        <>
                            <FormGroup label="TCP"
                                       validated={this.state.tcp_error ? "error" : "default"}
                                       helperText={_("Comma-separated ports, ranges, and aliases are accepted")}
                                       helperTextInvalid={this.state.tcp_error}>
                                <TextInput id="tcp-ports" type="text" onChange={this.validate}
                                           validated={this.state.tcp_error ? "error" : "default"}
                                           value={this.state.custom_tcp_value}
                                           placeholder={_("Example: 22,ssh,8080,5900-5910")} />
                            </FormGroup>

                            <FormGroup label="UDP"
                                       validated={this.state.udp_error ? "error" : "default"}
                                       helperText={_("Comma-separated ports, ranges, and aliases are accepted")}<commit_msg>firewall: rename aliases to services<commit_after>/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2018 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

import '../lib/patternfly/patternfly-4-cockpit.scss';
import cockpit from "cockpit";
import React from "react";
import ReactDOM from "react-dom";
import {
    Alert, Button,
    Breadcrumb, BreadcrumbItem,
    Checkbox,
    Card, CardTitle, CardHeader, CardActions, CardBody,
    DataList, DataListItem, DataListCell, DataListItemRow, DataListCheck, DataListItemCells,
    Flex, FlexItem,
    Form, FormGroup, FormHelperText,
    Radio, Split, SplitItem, Stack,
    TextInput, Title, Toolbar, ToolbarContent, ToolbarItem,
    Tooltip, Page, PageSection, PageSectionVariants, Modal,
} from '@patternfly/react-core';
import { ExclamationCircleIcon, TrashIcon } from '@patternfly/react-icons';

import firewall from "./firewall-client.js";
import { ListingTable } from 'cockpit-components-table.jsx';
import { ModalError } from "cockpit-components-inline-notification.jsx";
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
import { FirewallSwitch } from "./firewall-switch.jsx";

import { superuser } from "superuser";

import "./networking.scss";

const _ = cockpit.gettext;

superuser.reload_page_on_change();

function serviceRow(props) {
    let tcp = props.service.ports.filter(p => p.protocol.toUpperCase() == 'TCP');
    let udp = props.service.ports.filter(p => p.protocol.toUpperCase() == 'UDP');

    for (const s of props.service.includes) {
        if (firewall.services[s]) {
            tcp = tcp.concat(firewall.services[s].ports.filter(p => p.protocol.toUpperCase() == 'TCP'));
            udp = udp.concat(firewall.services[s].ports.filter(p => p.protocol.toUpperCase() == 'UDP'));
        }
    }

    function onRemoveService(event) {
        if (event.button !== 0)
            return;

        props.onRemoveService(props.service.id);
        event.stopPropagation();
    }

    const deleteButton = <Button key={props.service.id + "-delete-button"} variant="danger" onClick={onRemoveService} aria-label={cockpit.format(_("Remove service $0"), props.service.id)}><TrashIcon /></Button>;

    const columns = [
        {
            title: props.service.id, header: true
        },
        {
            title: <div key={props.service.id + "tcp"}>
                { tcp.map(p => p.port).join(', ') }
            </div>
        },
        {
            title: <div key={props.service.id + "udp"}>
                { udp.map(p => p.port).join(', ') }
            </div>
        },
    ];

    let description, includes;
    if (props.service.description)
        description = <p>{props.service.description}</p>;

    if (props.service.includes.length > 0) {
        includes = <>
            <h5>Included Services</h5>
            <ul>{props.service.includes.map(s => {
                const service = firewall.services[s];
                if (service && service.description)
                    return <li key={service.id}><strong>{service.id}</strong>: {service.description}</li>;
            })} </ul></>;
    }
    const simpleBody = <Split>
        <SplitItem key="description" isFilled>{description}{includes}</SplitItem>
        {!props.readonly && <SplitItem key="actions">{deleteButton}</SplitItem>}
    </Split>;

    return ({
        props: { key: props.service.id, 'data-row-id': props.service.id },
        columns,
        hasPadding: true,
        expandedContent: simpleBody,
    });
}

function portRow(props) {
    const columns = [
        {
            title: <i key={props.zone.id + "-additional-ports"}>{ _("Additional ports") }</i>
        },
        {
            title: props.zone.ports
                    .filter(p => p.protocol === "tcp")
                    .map(p => p.port)
                    .join(", ")
        },
        {
            title: props.zone.ports
                    .filter(p => p.protocol === "udp")
                    .map(p => p.port)
                    .join(", ")
        },
    ];
    return ({
        props: { key: props.zone.id + "-ports", 'data-row-id': props.zone.id + "-ports" },
        columns
    });
}

function ZoneSection(props) {
    function onRemoveZone(event) {
        if (event.button !== 0)
            return;

        event.stopPropagation();
        props.onRemoveZone(props.zone.id);
    }

    let deleteButton;
    if (props.readonly) {
        deleteButton = (
            <Tooltip id="tip-auth" content={ _("You are not authorized to modify the firewall.") }>
                <span>
                    <Button variant="danger"
                            aria-label={cockpit.format(_("Not authorized to remove zone $0"), props.zone.id)}
                            isDisabled icon={<TrashIcon />} />
                </span>
            </Tooltip>
        );
    } else {
        deleteButton = <Button variant="danger" onClick={onRemoveZone}
                               aria-label={cockpit.format(_("Remove zone $0"), props.zone.id)}
                               icon={<TrashIcon />} />;
    }

    const addServiceAction = (
        <Button variant="primary" onClick={() => props.openServicesDialog(props.zone.id, props.zone.id)} className="add-services-button" aria-label={cockpit.format(_("Add services to zone $0"), props.zone.id)}>
            {_("Add services")}
        </Button>
    );

    return <Card className="zone-section" data-id={props.zone.id}>
        <CardHeader className="zone-section-heading">
            <CardTitle>
                <h4>{ cockpit.format(_("$0 zone"), props.zone.id) }</h4>
                <div className="zone-section-targets">
                    { props.zone.interfaces.length > 0 && <span className="zone-section-target"><strong>{_("Interfaces")}</strong> {props.zone.interfaces.join(", ")}</span> }
                    { props.zone.source.length > 0 && <span className="zone-section-target"><strong>{_("Addresses")}</strong> {props.zone.source.join(", ")}</span> }
                </div>
            </CardTitle>
            { !firewall.readonly && <CardActions className="zone-section-buttons">{deleteButton}{addServiceAction}</CardActions> }
        </CardHeader>
        {props.zone.services.length > 0 &&
        <CardBody className="contains-list">
            <ListingTable columns={[{ title: _("Service"), props: { width: 40 } }, { title: _("TCP"), props: { width: 30 } }, { title: _("UDP"), props: { width: 30 } }]}
                          id={props.zone.id}
                          aria-label={props.zone.id}
                          variant="compact"
                          emptyCaption={_("There are no active services in this zone")}
                          rows={
                              props.zone.services.map(s => {
                                  if (s in firewall.services)
                                      return serviceRow({
                                          key: firewall.services[s].id,
                                          service: firewall.services[s],
                                          onRemoveService: service => props.onRemoveService(props.zone.id, service),
                                          readonly: firewall.readonly
                                      });
                              }).concat(
                                  props.zone.ports.length > 0
                                      ? portRow({
                                          key: props.zone.id + "-ports",
                                          zone: props.zone,
                                          readonly: firewall.readonly
                                      }) : [])
                                      .filter(Boolean)}

            />
        </CardBody>}
    </Card>;
}

class SearchInput extends React.Component {
    constructor(props) {
        super(props);
        this.onValueChanged = this.onValueChanged.bind(this);
        this.state = { value: props.value || "" };
    }

    onValueChanged(value) {
        this.setState({ value });

        if (this.timer)
            window.clearTimeout(this.timer);

        this.timer = window.setTimeout(() => {
            this.props.onChange(value);
            this.timer = null;
        }, 300);
    }

    render() {
        return (
            <Toolbar>
                <ToolbarContent>
                    <ToolbarItem variant="label">
                        {_("Filter services")}
                    </ToolbarItem>
                    <ToolbarItem>
                        <TextInput type="search"
                                   id={this.props.id}
                                   onChange={this.onValueChanged}
                                   value={this.state.value}
                        />
                    </ToolbarItem>
                </ToolbarContent>
            </Toolbar>
        );
    }
}

const renderPorts = service => {
    const tcpPorts = [];
    const udpPorts = [];
    function addPorts(ports) {
        for (const port of ports) {
            if (port.protocol === "tcp")
                tcpPorts.push(port.port);
            else
                udpPorts.push(port.port);
        }
    }
    addPorts(service.ports);
    for (const s of service.includes)
        addPorts(firewall.services[s].ports);

    return (
        <div className="service-list-item-text">
            { tcpPorts.length > 0 && <span className="service-ports tcp"><strong>TCP: </strong>{ tcpPorts.join(', ') }</span> }
            { udpPorts.length > 0 && <span className="service-ports udp"><strong>UDP: </strong>{ udpPorts.join(', ') }</span> }
        </div>
    );
};

class AddServicesModal extends React.Component {
    constructor() {
        super();

        this.state = {
            services: null,
            selected: new Set(),
            filter: "",
            custom: false,
            generate_custom_id: true,
            tcp_error: "",
            udp_error: "",
            avail_services: null,
            custom_id: "",
            custom_description: "",
            custom_tcp_ports: [],
            custom_udp_ports: [],
            custom_tcp_value: "",
            custom_udp_value: "",
            dialogError: null,
            dialogErrorDetail: null,
        };
        this.save = this.save.bind(this);
        this.onFilterChanged = this.onFilterChanged.bind(this);
        this.onToggleService = this.onToggleService.bind(this);
        this.setId = this.setId.bind(this);
        this.setDescription = this.setDescription.bind(this);
        this.getName = this.getName.bind(this);
        this.validate = this.validate.bind(this);
        this.createPorts = this.createPorts.bind(this);
        this.parseServices = this.parseServices.bind(this);
        this.onToggleType = this.onToggleType.bind(this);
    }

    createPorts() {
        const ret = [];
        this.state.custom_tcp_ports.forEach(port => ret.push([port, 'tcp']));
        this.state.custom_udp_ports.forEach(port => ret.push([port, 'udp']));
        return ret;
    }

    save(event) {
        let p;
        if (this.state.custom) {
            p = firewall.createService(this.state.custom_id, this.createPorts(), this.props.zoneId, this.state.custom_description);
        } else {
            p = firewall.addServices(this.props.zoneId, [...this.state.selected]);
        }
        p.then(() => this.props.close())
                .catch(error => {
                    this.setState({
                        dialogError: this.state.custom ? _("Failed to add port") : _("Failed to add service"),
                        dialogErrorDetail: error.name + ": " + error.message,
                    });
                });

        if (event)
            event.preventDefault();
        return false;
    }

    onToggleService(event, serviceId) {
        const service = serviceId;
        const enabled = event.target.checked;

        this.setState(oldState => {
            const selected = new Set(oldState.selected);

            if (enabled)
                selected.add(service);
            else
                selected.delete(service);

            return {
                selected: selected
            };
        });
    }

    /* Create list of services from /etc/services type file
     *
     * Return dictionary of services:
     *  - key => port number or port alias (80/http)
     *  - item => dictionary with 3 compulsory items:
     *      - name => port alias (http)
     *      - port => port number (80)
     *      - type => list of types (tcp/udp...)
     *      - description => _may be not present_ (Web Server)
     */
    parseServices(content) {
        if (!content) {
            console.warn("Couldn't read /etc/services");
            return [];
        }

        const ret = {};
        content.split('\n').forEach(line => {
            if (!line || line.startsWith("#"))
                return;
            const m = line.match(/^(\S+)\s+(\d+)\/(\S+).*?(#(.*))?$/);
            const new_port = { name: m[1], port: m[2], type: [m[3]] };
            if (m.length > 5 && m[5])
                new_port.description = m[5].trim();
            if (ret[m[1]])
                ret[m[1]].type.push(new_port.type[0]);
            else
                ret[m[1]] = new_port;
            if (ret[m[2]])
                ret[m[2]].type.push(new_port.type[0]);
            else
                ret[m[2]] = new_port;
        });
        return ret;
    }

    setId(value) {
        this.setState({
            custom_id: value,
            generate_custom_id: value.length === 0,
        });
    }

    setDescription(value) {
        this.setState({
            custom_description: value
        });
    }

    getName(port) {
        const known = this.state.avail_services[port];
        if (known)
            return known.name;
        else
            return port;
    }

    getPortNumber(port, type, avail) {
        if (!avail) {
            const num_p = Number(port);
            if (isNaN(num_p))
                return [0, _("Unknown service name")];
            else if (num_p <= 0 || num_p > 65535)
                return [0, _("Invalid port number")];
            else
                return [port, ""];
        } else if (avail.type.indexOf(type) < 0)
            return [0, _("Port number and type do not match")];
        else {
            return [avail.port, ""];
        }
    }

    validate(value, event) {
        let error = "";
        let targets = ['tcp', 'custom_tcp_ports', 'tcp_error', 'custom_tcp_value'];
        if (event.target.id === "udp-ports")
            targets = ['udp', 'custom_udp_ports', 'udp_error', 'custom_udp_value'];
        const new_ports = [];
        const event_id = event.target.id;

        this.setState(oldState => {
            const ports = value.split(',');
            ports.forEach((port) => {
                port = port.trim();
                if (!port)
                    return;
                let ports;
                if (port.indexOf("-") > -1) {
                    ports = port.split("-");
                    if (ports.length != 2) {
                        error = _("Invalid range");
                        return;
                    }
                    [ports[0], error] = this.getPortNumber(ports[0], targets[0], oldState.avail_services[ports[0]]);
                    if (!error) {
                        [ports[1], error] = this.getPortNumber(ports[1], targets[0], oldState.avail_services[ports[1]]);
                        if (!error) {
                            if (Number(ports[0]) >= Number(ports[1]))
                                error = _("Range must be strictly ordered");
                            else
                                new_ports.push(ports[0] + "-" + ports[1]);
                        }
                    }
                } else {
                    [ports, error] = this.getPortNumber(port, targets[0], oldState.avail_services[port]);
                    if (!error)
                        new_ports.push(ports);
                }
            });
            const newState = {
                [targets[1]]: new_ports,
                [targets[2]]: error,
                [targets[3]]: value
            };

            let all_ports = new_ports.concat(oldState.custom_udp_ports);
            if (event_id === "udp-ports")
                all_ports = oldState.custom_tcp_ports.concat(new_ports);

            if (oldState.generate_custom_id) {
                if (all_ports.length > 0)
                    newState.custom_id = "custom--" + all_ports.map(this.getName).join('-');
                else
                    newState.custom_id = "";
            }

            return newState;
        });
    }

    onToggleType(value, event) {
        this.setState({
            custom: event.target.value === "ports"
        });
    }

    componentDidMount() {
        firewall.getAvailableServices()
                .then(services => this.setState({ services }));
        cockpit.file('/etc/services').read()
                .done(content => this.setState({
                    avail_services: this.parseServices(content)
                }));
    }

    onFilterChanged(value) {
        this.setState({ filter: value.toLowerCase() });
    }

    render() {
        let services;
        if (this.state.filter && this.state.services && !isNaN(this.state.filter))
            services = this.state.services.filter(s => {
                for (const port of s.ports)
                    if (port.port === this.state.filter)
                        return true;
                return false;
            });
        else if (this.state.filter && this.state.services)
            services = this.state.services.filter(s => s.id.indexOf(this.state.filter) > -1);
        else
            services = this.state.services;

        // hide services which have been enabled in the zone
        if (services)
            services = services.filter(s => firewall.zones[this.props.zoneId].services.indexOf(s.id) === -1);

        const addText = this.state.custom ? _("Add ports") : _("Add services");
        const titleText = this.state.custom ? cockpit.format(_("Add ports to $0 zone"), this.props.zoneName) : cockpit.format(_("Add services to $0 zone"), this.props.zoneName);
        return (
            <Modal id="add-services-dialog" isOpen
                   position="top" variant="medium"
                   onClose={this.props.close}
                   title={titleText}
                   footer={<>
                       {
                           this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />
                       }
                       { !this.state.custom ||
                           <Alert variant="warning"
                               isInline
                               title={_("Adding custom ports will reload firewalld. A reload will result in the loss of any runtime-only configuration!")} />
                       }
                       <Button variant='primary' onClick={this.save} aria-label={titleText}>
                           {addText}
                       </Button>
                       <Button variant='link' className='btn-cancel' onClick={this.props.close}>
                           {_("Cancel")}
                       </Button>
                   </>}
            >
                <Form isHorizontal onSubmit={this.save}>
                    <FormGroup className="add-services-dialog-type" isInline>
                        <Radio name="type"
                               id="add-services-dialog--services"
                               value="services"
                               isChecked={!this.state.custom}
                               onChange={this.onToggleType}
                               label={_("Services")} />
                        <Radio name="type"
                               id="add-services-dialog--ports"
                               value="ports"
                               isChecked={this.state.custom}
                               onChange={this.onToggleType}
                               isDisabled={this.state.avail_services == null}
                               label={_("Custom ports")} />
                    </FormGroup>
                    { this.state.custom ||
                        <div>
                            { services ? (
                                <>
                                    <SearchInput id="filter-services-input"
                                                 value={this.state.filter}
                                                 onChange={this.onFilterChanged} />
                                    <DataList className="service-list" isCompact>
                                        {services.map(s => (
                                            <DataListItem key={s.id} aria-labelledby={s.id}>
                                                <DataListItemRow>
                                                    <DataListCheck aria-labelledby={s.id}
                                                                   isChecked={this.state.selected.has(s.id)}
                                                                   onChange={(value, event) => this.onToggleService(event, s.id)}
                                                                   id={"firewall-service-" + s.id}
                                                                   name={s.id + "-checkbox"} />
                                                    <DataListItemCells
                                                            dataListCells={[
                                                                <DataListCell key="service-list-item">
                                                                    <label htmlFor={"firewall-service-" + s.id}
                                                                           className="service-list-iteam-heading">
                                                                        {s.id}
                                                                    </label>
                                                                    {renderPorts(s)}
                                                                </DataListCell>,
                                                            ]} />
                                                </DataListItemRow>
                                            </DataListItem>
                                        ))}
                                    </DataList>
                                </>
                            ) : (
                                <EmptyStatePanel loading />
                            )}
                        </div>
                    }
                    { !this.state.custom ||
                        <>
                            <FormGroup label="TCP"
                                       validated={this.state.tcp_error ? "error" : "default"}
                                       helperText={_("Comma-separated ports, ranges, and services are accepted")}
                                       helperTextInvalid={this.state.tcp_error}>
                                <TextInput id="tcp-ports" type="text" onChange={this.validate}
                                           validated={this.state.tcp_error ? "error" : "default"}
                                           value={this.state.custom_tcp_value}
                                           placeholder={_("Example: 22,ssh,8080,5900-5910")} />
                            </FormGroup>

                            <FormGroup label="UDP"
                                       validated={this.state.udp_error ? "error" : "default"}
                                       helperText={_("Comma-separated ports, ranges, and services are accepted")}
{'fraction_non_alphanumeric': 0.1251043384433722, 'fraction_numerical': 0.004112461572914758, 'mean_word_length': 1.2965075506101267, 'pattern_counts': {'":': 2, '<': 241, '<?xml version=': 0, '>': 335, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 170, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6589858', 'n_tokens_mistral': 12613, 'n_tokens_neox': 12110, 'n_words': 2451}