\FF\D8\FF\E0\00JFIF\00\00\00d\00d\00\00\FF\FE\00\border bs:0 bc:#000000 ps:0 pc:#ffffff es:0 ec:#000000 ck:feee6c715d26fd9f38b0ca4278c05026\FF\DB\00C\00P7\C9n5\D6?\BD\9Ds\EBp\9F[`8m\B7)o\B5\E8\E6I\99\FE3]]A2\BA\8Cw\D6E\93\\DEv\C8\009\F2\F1NI?uc\\F5\EA\96k\xN<~buv\EA\C8\D7 \8B\84\CEcxI\BBg\AE\9E=\D6+n\EC\80\C8A\8C\AE\EB\CF\D5\DA\E9"2\A4\B9j5\EB\F3W\B63\96\B30Yu\DA\FC8\ED\DF\E7Ms\FB\F1\8E\B3\FA\EA\E8\E6(\883zs\F2_\8DFk\8Bh \00\8C\DCw\D3R\B5+6X\BA\B2\C4j\AB0\B4\FCMw\C2I\8E\9B\E3\A9~9u\FA\D3l\80\C8%p\EE\FDn2 \00 $\FEj\C4e\A9\DB\~\95\A7\A5\80EK\BB\8DDsP\00@@AD'k\CF\E8\DB\D2(\80\9AK\D3\85\D6lb\F2\BA\8C*\80\00)\95 59\A3R:\F3\CE"\B6\80\88\00\00i1u4\E9\F2\A6\A2\FACM\93WMb*\E0*\00\00\00\00\00(\A8\80\00\00\80\00\00\00\00\00\00\00\00\00\FF\D9 C/// File Manager

File Manager

Path: /opt/cloudlinux/venv/lib/python3.11/site-packages/clcommon/public_hooks/bin/

Viewing File: post_modify_domain.py

#!/opt/cloudlinux/venv/bin/python3 -bb
# -*- coding: utf-8 -*-

# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
#
"""
Domain POST modification hook.
This script must be called AFTER different domain-related
changes in control panel. See --help and docs for detailed
description of actions and parameters.
"""
import argparse
import os
import re
import sys

from clcommon.public_hooks.lib import ModifyDomainHook
from clcommon.public_hooks import setup_logger_and_sentry
# Use the shared username regex (allows dots) — bugbot finding a39cebf7
from clcommon.public_hooks.lib.helpers import _VALID_NAME_RE as _VALID_USERNAME_RE


# Domain-name validator. Unicode-aware: each label may contain letters and
# digits in any script (so IDN domains like `реселлер41.com` or `日本.jp`
# pass through). Plesk fires `phys_hosting_create` with the Unicode form,
# not the Punycode form (CLOS-4470); the prior strict-ASCII regex stalled
# `_wait_for_plesk_hooks` on i18n test fixtures because the dispatch to
# this script bailed before logging `ended post_create_domain ... <domain>`.
#
# Structural rules per RFC 1035/5890:
#   - 1..63 chars per label
#   - label starts and ends with a letter/digit (no leading/trailing `-`)
#   - labels joined by literal `.`
# Total length bound (1..253) is checked separately by `_validate_domain`.
#
# Forbidden chars (shell metas, path separators, control chars, whitespace)
# are naturally excluded because they're neither word chars nor `-`.
_DOMAIN_LABEL = r'[^\W_](?:[\w-]{0,61}[^\W_])?'
_VALID_DOMAIN_RE = re.compile(rf'\A{_DOMAIN_LABEL}(?:\.{_DOMAIN_LABEL})*\Z')


def _validate_username(value):
    if not value or len(value) > 32 or not _VALID_USERNAME_RE.fullmatch(value):
        sys.exit(f'Invalid username: {value!r}')


def _validate_domain(value):
    if not value or len(value) > 253 or not _VALID_DOMAIN_RE.fullmatch(value):
        sys.exit(f'Invalid domain: {value!r}')


def _validate_docroot(value):
    # NOTE(CLOS-4280): the AI-generated hardening in b525447 also rejected
    # non-absolute paths, but the cPanel SubDomain::changedocroot post-hook
    # legitimately forwards `dir` as a path relative to the user's home
    # (see cpanel_postchangesubdomaindocroot_main). Requiring isabs() here
    # broke the crontab-migration chain wired up in CLOS-3765 and caused
    # clsolo.python_tests.test_isolated_domains_hooks::test_crontab_migration_and_cleanup
    # to fail (Tests-all #7057). Rejecting NUL and any `..` segment is
    # sufficient: this script never opens the path — it only forwards it
    # to listeners that resolve docroots against the user's home.
    #
    # A leading `-` is also rejected: the docroot is forwarded as the value of
    # `--new-docroot` with no `--` separator, so a `-`-leading value misparses
    # as an option and skips the hook. Legitimate relative docroots (e.g.
    # `public_html/sub`) never start with `-`, so this does not re-break the
    # CLOS-4280 relative-path case above.
    if (not value or value.startswith('-')
            or '\0' in value or '..' in value.split(os.sep)):
        sys.exit(f'Invalid docroot path: {value!r}')


if __name__ == '__main__':
    setup_logger_and_sentry()

    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest='command')

    create_domain = subparsers.add_parser(
        'create', help="[NOT FOR VENDORS PANELS INTEGRATION] Call when domain record is created.")
    create_domain.add_argument('--username', '-u', required=True, help='User that owns domain.')
    create_domain.add_argument('--domain', '-ds', required=True,
                               help='Domain name e.g.: `userdomain.com`')

    modify_domain = subparsers.add_parser(
        'modify', help="Call when domain record is updated (e.g "
                       "all sites that have worked on old domain "
                       "now work on a new one).")
    modify_domain.add_argument('--username', '-u', required=True, help='User that owns domain.')
    modify_domain.add_argument('--domain', '-ds', required=True,
                               help='Domain name before rename, e.g.: `userdomain.com`')
    modify_domain.add_argument('--new-domain', '-dt',
                               help='Domain name after rename, e.g.: `userdomain2.eu`')
    # TODO: how to make this better?'
    modify_domain.add_argument(
        '--include-subdomains', required=False, default=False, action='store_true',
        help='If set, we will also process all domains '
             'that end with domain name (subdomains). '
             'E.g. when renaming olddomain.com -> newdomain.eu '
             'we will also rename sub.olddomain.com to sub.newdomain.eu')
    modify_domain.add_argument('--old-docroot',
                               help='Old document root path for the domain.')
    modify_domain.add_argument('--new-docroot',
                               help='New document root path for the domain.')

    modify_domain = subparsers.add_parser(
        'delete', help="[NOT FOR INTEGRATED CONTROL PANELS] Call when domain record is deleted.")
    modify_domain.add_argument('--username', '-u', required=True, help='User that owns domain.')
    modify_domain.add_argument('--domain', '-ds', required=True,
                               help='Domain name before rename, e.g.: `userdomain.com`')

    args = parser.parse_args()

    hook = ModifyDomainHook()
    if args.command == 'create':
        _validate_username(args.username)
        _validate_domain(args.domain)
        hook.post_create_domain(
            args.username, args.domain)
    elif args.command == 'modify':
        _validate_username(args.username)
        _validate_domain(args.domain)
        if args.new_domain:
            _validate_domain(args.new_domain)
        if args.old_docroot:
            _validate_docroot(args.old_docroot)
        if args.new_docroot:
            _validate_docroot(args.new_docroot)
        kwargs = {'include_subdomains': args.include_subdomains}
        if args.old_docroot:
            kwargs['old_docroot'] = args.old_docroot
        if args.new_docroot:
            kwargs['new_docroot'] = args.new_docroot
        hook.post_modify_domain(
            args.username, args.domain, args.new_domain, **kwargs)
    elif args.command == 'delete':
        _validate_username(args.username)
        _validate_domain(args.domain)
        hook.post_delete_domain(args.username, args.domain)
    else:
        raise NotImplementedError